From 0f58f2efbddeea4878bececb7726bba7d652f146 Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 14 Oct 2022 11:43:36 -0700 Subject: [PATCH 001/149] Fix JS errors when the OBW business step is accessed directly via URL (#35045) * Set default value for product types * Add changelog --- .../steps/business-details/flows/selective-bundle/index.js | 2 +- .../woocommerce/changelog/fix-34974-obw-steps-break-via-url | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js index ba3aba4599b..26d24a79218 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js @@ -819,7 +819,7 @@ export const BusinessFeaturesList = compose( ? getInstallableExtensions( { freeExtensionBundleByCategory: freeExtensions, country, - productTypes: profileItems.product_types, + productTypes: profileItems.product_types || [], } ) : []; const hasInstallableExtensions = installableExtensions.some( diff --git a/plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url b/plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url new file mode 100644 index 00000000000..5f06728d05e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix JS error when the business step is accessed directly via URL without completing the previous steps From 1eecefb715f82933aca47de88375959d0e686031 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Fri, 14 Oct 2022 14:23:39 -0500 Subject: [PATCH 002/149] Update version of auto-assign-reviewer (#35104) * Update version of auto-assign-reviewer Update version of auto-assign-reviewer action to one that fixes the bug with team reviewers. * Update community pr assigner config for team syntax The new version of community pr assigner uses `team: ` prefix to designate teams. --- .github/project-community-pr-assigner.yml | 2 +- .github/workflows/community-label.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml index f28936a753b..e0f2f4e12a2 100644 --- a/.github/project-community-pr-assigner.yml +++ b/.github/project-community-pr-assigner.yml @@ -1,3 +1,3 @@ ".github/*": - - atlas + - team: atlas diff --git a/.github/workflows/community-label.yml b/.github/workflows/community-label.yml index 9a388fca414..213dd2a0b49 100644 --- a/.github/workflows/community-label.yml +++ b/.github/workflows/community-label.yml @@ -32,7 +32,7 @@ jobs: - name: "If community PR, assign a reviewer" if: github.event.pull_request && steps.check.outputs.is-community == 'yes' - uses: shufo/auto-assign-reviewer-by-files@24a9fcbd5c51c4403b64c8b6e087c824b67a5c35 + uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6 with: config: ".github/project-community-pr-assigner.yml" token: ${{ secrets.GITHUB_TOKEN }} From 0cbc45ba85c324cb19c1f4c6c6059888c53b384b Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Fri, 14 Oct 2022 15:38:18 -0400 Subject: [PATCH 003/149] Handle ambiguous dates (with no offset) in DateTimePickerControl (#35077) * Add tests for ambiguous and unambiguous UTC dates * Assume UTC for ambiguous dates * Changelog * Remove console statements --- ...ix-date-time-picker-control-ambiguous-date | 4 ++ .../date-time-picker-control.tsx | 19 ++++++--- .../date-time-picker-control/test/index.tsx | 40 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date diff --git a/packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date b/packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date new file mode 100644 index 00000000000..113cac85c15 --- /dev/null +++ b/packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Assume ambiguous dates passed into DateTimePickerControl are UTC. diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index 186604c4069..c040c3fc6b1 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -75,20 +75,28 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { null ); - function parseMomentIso( dateString?: string | null ): Moment { - return moment( dateString, moment.ISO_8601, true ); + function parseMomentIso( + dateString?: string | null, + assumeLocalTime = false + ): Moment { + if ( assumeLocalTime ) { + return moment( dateString, moment.ISO_8601, true ).utc(); + } + + return moment.utc( dateString, moment.ISO_8601, true ); } function parseMoment( dateString?: string | null ): Moment { + // parse input date string as local time return moment( dateString, dateTimeFormat ); } function formatMomentIso( momentDate: Moment ): string { - return momentDate.toISOString(); + return momentDate.utc().toISOString(); } function formatMoment( momentDate: Moment ): string { - return momentDate.format( dateTimeFormat ); + return momentDate.local().format( dateTimeFormat ); } function hasFocusLeftInputAndDropdownContent( @@ -272,8 +280,9 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { : undefined } onChange={ ( date: string ) => { + // the picker returns the date in local time const formattedDate = formatMoment( - parseMomentIso( date ) + parseMomentIso( date, true ) ); changeImmediate( formattedDate, true ); } } diff --git a/packages/js/components/src/date-time-picker-control/test/index.tsx b/packages/js/components/src/date-time-picker-control/test/index.tsx index 7050c2e92eb..2c320fb64fa 100644 --- a/packages/js/components/src/date-time-picker-control/test/index.tsx +++ b/packages/js/components/src/date-time-picker-control/test/index.tsx @@ -106,6 +106,46 @@ describe( 'DateTimePickerControl', () => { ); } ); + it( 'should assume ambiguous dates are UTC', () => { + const ambiguousISODateTimeString = '2202-09-15T22:30:40'; + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + + expect( input?.value ).toBe( + moment + .utc( ambiguousISODateTimeString ) + .local() + .format( default24HourDateTimeFormat ) + ); + } ); + + it( 'should handle unambiguous UTC dates', () => { + const unambiguousISODateTimeString = '2202-09-15T22:30:40Z'; + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + + expect( input?.value ).toBe( + moment + .utc( unambiguousISODateTimeString ) + .local() + .format( default24HourDateTimeFormat ) + ); + } ); + it( 'should use the default 12 hour date time format', () => { const dateTime = moment( '2022-09-15 02:30:40' ); From 8747ea29952121909f37c65015c4f60c4bf0b39c Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Fri, 14 Oct 2022 15:22:27 -0500 Subject: [PATCH 004/149] Remove latest stable version from README (#35056) * Remove latest stable version from README * Add change file --- plugins/woocommerce/README.md | 1 - plugins/woocommerce/changelog/update-readme-stable | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/update-readme-stable diff --git a/plugins/woocommerce/README.md b/plugins/woocommerce/README.md index a6e050a91b7..04fe227e5b9 100644 --- a/plugins/woocommerce/README.md +++ b/plugins/woocommerce/README.md @@ -2,7 +2,6 @@

license -Latest Stable Version WordPress.org downloads WordPress.org rating Build Status diff --git a/plugins/woocommerce/changelog/update-readme-stable b/plugins/woocommerce/changelog/update-readme-stable new file mode 100644 index 00000000000..2df31d83341 --- /dev/null +++ b/plugins/woocommerce/changelog/update-readme-stable @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Update the README, no changelog required + + From e470c62650553a933b615906db82a9139ed14116 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Fri, 14 Oct 2022 16:48:03 -0400 Subject: [PATCH 005/149] Add date-only mode to DateTimePickerControl (#35066) * Remove border from top of date picker * Add isDateOnlyPicker, rename is12HourPicker * Add tests for isDateOnlyPicker * Add date only story --- ...dd-date-time-picker-control-date-only-mode | 4 + .../date-time-picker-control.scss | 4 + .../date-time-picker-control.tsx | 75 ++++++++++++------- .../stories/index.tsx | 7 ++ .../date-time-picker-control/test/index.tsx | 49 ++++++++++-- 5 files changed, 104 insertions(+), 35 deletions(-) create mode 100644 packages/js/components/changelog/add-date-time-picker-control-date-only-mode diff --git a/packages/js/components/changelog/add-date-time-picker-control-date-only-mode b/packages/js/components/changelog/add-date-time-picker-control-date-only-mode new file mode 100644 index 00000000000..0e0d26f415a --- /dev/null +++ b/packages/js/components/changelog/add-date-time-picker-control-date-only-mode @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add date-only mode to DateTimePickerControl. diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss index 740ed510c67..feb98a25427 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss @@ -4,4 +4,8 @@ .woocommerce-date-time-picker-control__input-control__suffix { padding-right: 8px; } + + .components-datetime__date { + border-top: 0; + } } diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index c040c3fc6b1..69c40f7464b 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -6,7 +6,6 @@ import { useState, useEffect, useLayoutEffect, - useCallback, useRef, } from '@wordpress/element'; import { Icon, calendar } from '@wordpress/icons'; @@ -16,12 +15,14 @@ import { sprintf, __ } from '@wordpress/i18n'; import { useDebounce, useInstanceId } from '@wordpress/compose'; import { BaseControl, - Dropdown, + DatePicker, DateTimePicker as WpDateTimePicker, + Dropdown, // @ts-expect-error `__experimentalInputControl` does exist. __experimentalInputControl as InputControl, } from '@wordpress/components'; +export const defaultDateFormat = 'MM/DD/YYYY'; export const default12HourDateTimeFormat = 'MM/DD/YYYY h:mm a'; export const default24HourDateTimeFormat = 'MM/DD/YYYY H:mm'; @@ -34,7 +35,8 @@ export type DateTimePickerControlProps = { currentDate?: string | null; dateTimeFormat?: string; disabled?: boolean; - is12Hour?: boolean; + isDateOnlyPicker?: boolean; + is12HourPicker?: boolean; onChange?: DateTimePickerControlOnChangeHandler; onBlur?: () => void; label?: string; @@ -45,10 +47,9 @@ export type DateTimePickerControlProps = { export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { currentDate, - is12Hour = true, - dateTimeFormat = is12Hour - ? default12HourDateTimeFormat - : default24HourDateTimeFormat, + isDateOnlyPicker = false, + is12HourPicker = true, + dateTimeFormat, disabled = false, onChange, onBlur, @@ -75,6 +76,22 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { null ); + const displayFormat = ( () => { + if ( dateTimeFormat ) { + return dateTimeFormat; + } + + if ( isDateOnlyPicker ) { + return defaultDateFormat; + } + + if ( is12HourPicker ) { + return default12HourDateTimeFormat; + } + + return default24HourDateTimeFormat; + } )(); + function parseMomentIso( dateString?: string | null, assumeLocalTime = false @@ -88,7 +105,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { function parseMoment( dateString?: string | null ): Moment { // parse input date string as local time - return moment( dateString, dateTimeFormat ); + return moment( dateString, displayFormat ); } function formatMomentIso( momentDate: Moment ): string { @@ -96,7 +113,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } function formatMoment( momentDate: Moment ): string { - return momentDate.local().format( dateTimeFormat ); + return momentDate.local().format( displayFormat ); } function hasFocusLeftInputAndDropdownContent( @@ -211,7 +228,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } else { changeImmediate( currentDate || '', fireOnChange ); } - }, [ currentDate, dateTimeFormat ] ); + }, [ currentDate, displayFormat ] ); return ( = ( { /> ) } - renderContent={ () => ( - { - // the picker returns the date in local time - const formattedDate = formatMoment( - parseMomentIso( date, true ) - ); - changeImmediate( formattedDate, true ); - } } - is12Hour={ is12Hour } - /> - ) } + renderContent={ () => { + const Picker = isDateOnlyPicker ? DatePicker : WpDateTimePicker; + + return ( + { + // the picker returns the date in local time + const formattedDate = formatMoment( + parseMomentIso( date, true ) + ); + changeImmediate( formattedDate, true ); + } } + is12Hour={ is12HourPicker } + /> + ); + } } /> ); }; diff --git a/packages/js/components/src/date-time-picker-control/stories/index.tsx b/packages/js/components/src/date-time-picker-control/stories/index.tsx index 23bd6600c7f..2fd1d3fea21 100644 --- a/packages/js/components/src/date-time-picker-control/stories/index.tsx +++ b/packages/js/components/src/date-time-picker-control/stories/index.tsx @@ -90,3 +90,10 @@ Controlled.decorators = [ ); }, ]; + +export const ControlledDateOnly = Template.bind( {} ); +ControlledDateOnly.args = { + ...Controlled.args, + isDateOnlyPicker: true, +}; +ControlledDateOnly.decorators = Controlled.decorators; diff --git a/packages/js/components/src/date-time-picker-control/test/index.tsx b/packages/js/components/src/date-time-picker-control/test/index.tsx index 2c320fb64fa..2750fa4d55d 100644 --- a/packages/js/components/src/date-time-picker-control/test/index.tsx +++ b/packages/js/components/src/date-time-picker-control/test/index.tsx @@ -96,7 +96,7 @@ describe( 'DateTimePickerControl', () => { const { container } = render( ); @@ -112,7 +112,7 @@ describe( 'DateTimePickerControl', () => { const { container } = render( ); @@ -132,7 +132,7 @@ describe( 'DateTimePickerControl', () => { const { container } = render( ); @@ -152,7 +152,7 @@ describe( 'DateTimePickerControl', () => { const { container } = render( ); @@ -184,14 +184,14 @@ describe( 'DateTimePickerControl', () => { const { container, rerender } = render( ); rerender( ); @@ -229,9 +229,23 @@ describe( 'DateTimePickerControl', () => { ); } ); - it( 'should set the date time picker popup to 12 hour mode', async () => { + it( 'should set the picker popup to date and time by default', async () => { + const { container } = render( ); + + const input = container.querySelector( 'input' ); + + userEvent.click( input! ); + + await waitFor( () => + expect( + container.querySelector( '.components-datetime' ) + ).toBeInTheDocument() + ); + } ); + + it( 'should set the picker to 12 hour mode', async () => { const { container } = render( - + ); const input = container.querySelector( 'input' ); @@ -247,6 +261,25 @@ describe( 'DateTimePickerControl', () => { ); } ); + it( 'should set the picker popup to date only', async () => { + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + + userEvent.click( input! ); + + await waitFor( () => { + expect( + container.querySelector( '.components-datetime' ) + ).not.toBeInTheDocument(); + expect( + container.querySelector( '.components-datetime__date' ) + ).toBeInTheDocument(); + } ); + } ); + it( 'should call onBlur when losing focus', async () => { const onBlurHandler = jest.fn(); From 13a5ba2f1deca43e923ba9de5ec67b4c7646e052 Mon Sep 17 00:00:00 2001 From: Moon Date: Mon, 17 Oct 2022 11:23:55 -0700 Subject: [PATCH 006/149] Optimize query usage in the Onboarding tasks (#35065) * Optimize is_complete() method * Replaed WC_Product_Query with wp_count_posts, which has more optimized query * Optimize TaskList::is_complete -- return early when false instead of running through all the tasks * Cache is_complete() method for the tasks with db query * Add changelog --- .../fix-34833-slow-onboarding-task-list-query | 4 ++++ .../Admin/Features/OnboardingTasks/TaskList.php | 14 ++++++-------- .../OnboardingTasks/Tasks/AdditionalPayments.php | 13 ++++++++++++- .../Features/OnboardingTasks/Tasks/Payments.php | 13 ++++++++++++- .../Features/OnboardingTasks/Tasks/Products.php | 12 ++---------- .../Admin/Features/OnboardingTasks/Tasks/Tax.php | 16 +++++++++++++--- 6 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query diff --git a/plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query b/plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query new file mode 100644 index 00000000000..981c1a4a9e4 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Optimize query usage in the Onboarding tasks diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index e350d5d576c..2ea980c9106 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -239,15 +239,13 @@ class TaskList { * @return bool */ public function is_complete() { - $viewable_tasks = $this->get_viewable_tasks(); + foreach ( $this->get_viewable_tasks() as $viewable_task ) { + if ( $viewable_task->is_complete() === false ) { + return false; + } + } - return array_reduce( - $viewable_tasks, - function( $is_complete, $task ) { - return ! $task->is_complete() ? false : $is_complete; - }, - true - ); + return true; } /** diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php index 36617070dc8..9465bb3e94a 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php @@ -12,6 +12,13 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayme */ class AdditionalPayments extends Payments { + /** + * Used to cache is_complete() method result. + * @var null + */ + private $is_complete_result = null; + + /** * ID. * @@ -60,7 +67,11 @@ class AdditionalPayments extends Payments { * @return bool */ public function is_complete() { - return self::has_gateways(); + if ( $this->is_complete_result === null ) { + $this->is_complete_result = self::has_gateways(); + } + + return $this->is_complete_result; } /** diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php index 6cd523c6f63..5570ed8dc2e 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php @@ -10,6 +10,13 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; * Payments Task */ class Payments extends Task { + + /** + * Used to cache is_complete() method result. + * @var null + */ + private $is_complete_result = null; + /** * ID. * @@ -67,7 +74,11 @@ class Payments extends Task { * @return bool */ public function is_complete() { - return self::has_gateways(); + if ( $this->is_complete_result === null ) { + $this->is_complete_result = self::has_gateways(); + } + + return $this->is_complete_result; } /** diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php index 56ba733aa56..bae2bafda6f 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php @@ -167,15 +167,7 @@ class Products extends Task { * @return bool */ public static function has_products() { - $product_query = new \WC_Product_Query( - array( - 'limit' => 1, - 'return' => 'ids', - 'status' => array( 'publish' ), - ) - ); - $products = $product_query->get_products(); - - return count( $products ) !== 0; + $counts = wp_count_posts('product'); + return isset( $counts->publish ) && $counts->publish > 0; } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index ca06c415616..ca508bd880e 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -13,6 +13,12 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets; */ class Tax extends Task { + /** + * Used to cache is_complete() method result. + * @var null + */ + private $is_complete_result = null; + /** * Constructor * @@ -114,9 +120,13 @@ class Tax extends Task { * @return bool */ public function is_complete() { - return get_option( 'wc_connect_taxes_enabled' ) || - count( TaxDataStore::get_taxes( array() ) ) > 0 || - get_option( 'woocommerce_no_sales_tax' ) !== false; + if ( $this->is_complete_result === null ) { + $this->is_complete_result = get_option( 'wc_connect_taxes_enabled' ) || + count( TaxDataStore::get_taxes( array() ) ) > 0 || + get_option( 'woocommerce_no_sales_tax' ) !== false; + } + + return $this->is_complete_result; } /** From 868f581af8309d5a22277c1f01dda18a0cc3894f Mon Sep 17 00:00:00 2001 From: Greg <71906536+zhongruige@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:24:07 -0600 Subject: [PATCH 007/149] Prepare api-core-tests for release (#35001) --- packages/js/api-core-tests/CHANGELOG.md | 2 ++ packages/js/api-core-tests/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/js/api-core-tests/CHANGELOG.md b/packages/js/api-core-tests/CHANGELOG.md index fd5c09f3ee7..630728985c5 100644 --- a/packages/js/api-core-tests/CHANGELOG.md +++ b/packages/js/api-core-tests/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +# 1.0.0 + ## Changed - Bumped jest version to v27 - Used the jest packaged bundled in this module to run tests diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json index 8ee776540d6..d176de4d7b6 100644 --- a/packages/js/api-core-tests/package.json +++ b/packages/js/api-core-tests/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/api-core-tests", - "version": "0.1.0", + "version": "1.0.0", "description": "API tests for WooCommerce", "main": "index.js", "engines": { From c6eab3b4b297c1ce946f60984097138fb5d26fe6 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Mon, 17 Oct 2022 13:48:56 -0500 Subject: [PATCH 008/149] Fix the match pattern for the version bump tool (#34982) * Update pattern in version bump tool to use two digits * Update README for version bump for pnpm7 parameter order --- tools/version-bump/README.md | 2 +- tools/version-bump/lib/update.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/version-bump/README.md b/tools/version-bump/README.md index b588a3fd586..3bb50735fd3 100644 --- a/tools/version-bump/README.md +++ b/tools/version-bump/README.md @@ -9,7 +9,7 @@ Bump WooCommerce to version 7.1.0: ``` -pnpm run version --filter version-bump -- bump woocommerce -v 7.1.0 +pnpm --filter version-bump run version bump woocommerce -v 7.1.0 ``` **Arguments**: diff --git a/tools/version-bump/lib/update.ts b/tools/version-bump/lib/update.ts index 527b466016b..e94235f4ed6 100644 --- a/tools/version-bump/lib/update.ts +++ b/tools/version-bump/lib/update.ts @@ -25,7 +25,7 @@ export const updateReadmeStableTag = async ( const readmeContents = await readFile( filePath, 'utf8' ); const updatedReadmeContents = readmeContents.replace( - /Stable tag: \d.\d.\d\n/m, + /Stable tag: \d+\.\d+\.\d+\n/m, `Stable tag: ${ nextVersion }\n` ); @@ -50,7 +50,7 @@ export const updateReadmeChangelog = async ( const readmeContents = await readFile( filePath, 'utf8' ); const updatedReadmeContents = readmeContents.replace( - /= \d.\d.\d \d\d\d\d-XX-XX =\n/m, + /= \d+\.\d+\.\d+ \d\d\d\d-XX-XX =\n/m, `= ${ nextVersion } ${ new Date().getFullYear() }-XX-XX =\n` ); @@ -86,7 +86,7 @@ export const updateClassPluginFile = async ( const classPluginFileContents = await readFile( filePath, 'utf8' ); const updatedClassPluginFileContents = classPluginFileContents.replace( - /public \$version = '\d.\d.\d';\n/m, + /public \$version = '\d+\.\d+\.\d+';\n/m, `public $version = '${ nextVersion }';\n` ); @@ -142,7 +142,7 @@ export const updatePluginFile = async ( const pluginFileContents = await readFile( filePath, 'utf8' ); const updatedPluginFileContents = pluginFileContents.replace( - /Version: \d.\d.\d.*\n/m, + /Version: \d+\.\d+\.\d+.*\n/m, `Version: ${ nextVersion }\n` ); await writeFile( filePath, updatedPluginFileContents ); From 9fc0935e987b505967ebcb3b66ca29573b85b0f5 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Mon, 17 Oct 2022 16:50:18 -0400 Subject: [PATCH 009/149] Remove non-gmt scheduled sale properties from Product type (#35096) --- .../update-data-products-exclude-non-gmt-scheduled-sale | 4 ++++ packages/js/data/src/products/types.ts | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale diff --git a/packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale b/packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale new file mode 100644 index 00000000000..1db6a633809 --- /dev/null +++ b/packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Remove `Product` `date_on_sale_from` and `date_on_sale_to` properties. Use `date_on_sale_from_gmt` and `date_on_sale_to_gmt` instead. diff --git a/packages/js/data/src/products/types.ts b/packages/js/data/src/products/types.ts index d42c781ed12..57da249e4b1 100644 --- a/packages/js/data/src/products/types.ts +++ b/packages/js/data/src/products/types.ts @@ -59,9 +59,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit< description: string; short_description: string; sku: string; - date_on_sale_from: string | null; date_on_sale_from_gmt: string | null; - date_on_sale_to: string | null; date_on_sale_to_gmt: string | null; virtual: boolean; downloadable: boolean; From 00368924df68063f0299fadd9f845da5832747b0 Mon Sep 17 00:00:00 2001 From: Paulo Arromba <17236129+wavvves@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:36:05 +0100 Subject: [PATCH 010/149] Update WooCommerce Blocks package to 8.7.2 (#35101) * Update WooCommerce Blocks package to 8.7.2 Co-authored-by: Niels Lange --- .../changelog/update-woocommerce-blocks-8.7.2 | 4 ++++ plugins/woocommerce/composer.json | 2 +- plugins/woocommerce/composer.lock | 14 +++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 new file mode 100644 index 00000000000..8ba98efe1ee --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Comment: Update WooCommerce Blocks to 8.7.2 diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 19958785f93..71dd3e11190 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.4.2", - "woocommerce/woocommerce-blocks": "8.7.1" + "woocommerce/woocommerce-blocks": "8.7.2" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 32a6ca40efe..2bd7f28345a 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -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": "9e0e2721da4db7a3c7474c3e2f14c236", + "content-hash": "d1c35e053ae34c5434635a435e326266", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v8.7.1", + "version": "v8.7.2", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "5530ea25a060cb0eaf9e930f9097057fb820f22e" + "reference": "2ff8573d5426ebee180e50539f113c211362d1b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/5530ea25a060cb0eaf9e930f9097057fb820f22e", - "reference": "5530ea25a060cb0eaf9e930f9097057fb820f22e", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/2ff8573d5426ebee180e50539f113c211362d1b9", + "reference": "2ff8573d5426ebee180e50539f113c211362d1b9", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.1" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.2" }, - "time": "2022-10-12T15:27:50+00:00" + "time": "2022-10-14T12:13:45+00:00" } ], "packages-dev": [ From d0e277e80b1b96f6c485fd13886b2951d1967cc9 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Tue, 18 Oct 2022 09:20:22 -0500 Subject: [PATCH 011/149] Add PR_ASSIGN_TOKEN to contributors action (#35137) Adds PR_ASSIGN_TOKEN with repo permissions to the community contributors actions so that the bot has permission to make team assignments --- .github/workflows/community-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/community-label.yml b/.github/workflows/community-label.yml index 213dd2a0b49..221753b62e3 100644 --- a/.github/workflows/community-label.yml +++ b/.github/workflows/community-label.yml @@ -35,4 +35,4 @@ jobs: uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6 with: config: ".github/project-community-pr-assigner.yml" - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PR_ASSIGN_TOKEN }} From 3c6681054a2ab2dc951d68e48d1cf1bcb18aaa23 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Tue, 18 Oct 2022 08:55:31 -0700 Subject: [PATCH 012/149] Add product management description (#34961) * Add label prop to rich text editor * Create feature class and enqueue styles * Add description editor and parsing * Fix up incorrect context for product data * Add styling to rich text editors in product form * Fix editor initialization on new product * Add changelog entries * Use trunk lock file * Add component changelog entry * Update lock file * Register block store provider in tests * Fix up phpcs errors in product feature file --- .../add-product-management-description | 4 + .../src/rich-text-editor/rich-text-editor.tsx | 7 +- .../src/rich-text-editor/style.scss | 8 +- .../add-product-management-description | 4 + packages/js/data/src/products/resolvers.ts | 5 +- .../layout/product-section-layout.scss | 3 +- .../sections/product-details-section.tsx | 20 +- .../test/product-details-section.spec.tsx | 39 +- plugins/woocommerce-admin/package.json | 5 +- .../add-product-management-description | 4 + .../NewProductManagementExperience.php | 37 + pnpm-lock.yaml | 631 ++++++++---------- 12 files changed, 410 insertions(+), 357 deletions(-) create mode 100644 packages/js/components/changelog/add-product-management-description create mode 100644 packages/js/data/changelog/add-product-management-description create mode 100644 plugins/woocommerce/changelog/add-product-management-description create mode 100644 plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php diff --git a/packages/js/components/changelog/add-product-management-description b/packages/js/components/changelog/add-product-management-description new file mode 100644 index 00000000000..7e220824778 --- /dev/null +++ b/packages/js/components/changelog/add-product-management-description @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add label prop to rich text editor diff --git a/packages/js/components/src/rich-text-editor/rich-text-editor.tsx b/packages/js/components/src/rich-text-editor/rich-text-editor.tsx index 0db10c3439d..d256503271c 100644 --- a/packages/js/components/src/rich-text-editor/rich-text-editor.tsx +++ b/packages/js/components/src/rich-text-editor/rich-text-editor.tsx @@ -1,9 +1,9 @@ /** * External dependencies */ +import { BaseControl, SlotFillProvider } from '@wordpress/components'; import { BlockEditorProvider } from '@wordpress/block-editor'; import { BlockInstance } from '@wordpress/blocks'; -import { SlotFillProvider } from '@wordpress/components'; import { debounce } from 'lodash'; import { createElement, @@ -28,12 +28,14 @@ registerBlocks(); type RichTextEditorProps = { blocks: BlockInstance[]; + label?: string; onChange: ( changes: BlockInstance[] ) => void; entryId?: string; }; export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { blocks, + label, onChange, } ) => { const blocksRef = useRef( blocks ); @@ -61,6 +63,9 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { return (

+ { label && ( + { label } + ) } ) { export function* getProduct( productId: number ) { try { const product: Product = yield apiFetch( { - path: `${ WC_PRODUCT_NAMESPACE }/${ productId }`, + path: addQueryArgs( `${ WC_PRODUCT_NAMESPACE }/${ productId }`, { + context: 'edit', + } ), method: 'GET', } ); diff --git a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss index 0759fa9922a..82fa017706b 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss @@ -13,7 +13,8 @@ } } - .components-base-control { + .components-base-control, + .woocommerce-rich-text-editor { &:not(:first-child) { margin-top: $gap-large - $gap-smaller; margin-bottom: 0; diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index 43fffdc0f4f..1cfb32776ca 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -12,7 +12,11 @@ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { cleanForSlug } from '@wordpress/url'; -import { EnrichedLabel, useFormContext } from '@woocommerce/components'; +import { + EnrichedLabel, + useFormContext, + __experimentalRichTextEditor as RichTextEditor, +} from '@woocommerce/components'; import { Product, ProductCategory, @@ -20,6 +24,7 @@ import { WCDataSelector, } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; +import { BlockInstance, serialize, parse } from '@wordpress/blocks'; /** * Internal dependencies @@ -33,10 +38,13 @@ import { CategoryField } from '../fields/category-field'; const PRODUCT_DETAILS_SLUG = 'product-details'; export const ProductDetailsSection: React.FC = () => { - const { getInputProps, values, touched, errors, setValue } = + const { getInputProps, values, setValue, touched, errors } = useFormContext< Product >(); const [ showProductLinkEditModal, setShowProductLinkEditModal ] = useState( false ); + const [ descriptionBlocks, setDescriptionBlocks ] = useState< + BlockInstance[] + >( parse( values.description || '' ) ); const { permalinkPrefix, permalinkSuffix } = useSelect( ( select: WCDataSelector ) => { const { getPermalinkParts } = select( PRODUCTS_STORE_NAME ); @@ -162,6 +170,14 @@ export const ProductDetailsSection: React.FC = () => { } /> ) } + { + setDescriptionBlocks( blocks ); + setValue( 'description', serialize( blocks ) ); + } } + /> diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx index 77ec61bf9c7..5ce9b393cd3 100644 --- a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx +++ b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx @@ -1,11 +1,20 @@ /** * External dependencies */ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { useSelect } from '@wordpress/data'; +import { createRegistry, RegistryProvider, useSelect } from '@wordpress/data'; import { Form } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; +import { render, screen } from '@testing-library/react'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { store as blockEditorStore } from '@wordpress/block-editor'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { store as coreDataStore } from '@wordpress/core-data'; +// eslint-disable-next-line @woocommerce/dependency-group +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -19,6 +28,14 @@ jest.mock( '@wordpress/data', () => ( { useSelect: jest.fn(), } ) ); +const registry = createRegistry(); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +registry.register( coreDataStore ); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +registry.register( blockEditorStore ); + describe( 'ProductDetailsSection', () => { const useSelectMock = useSelect as jest.Mock; @@ -43,9 +60,11 @@ describe( 'ProductDetailsSection', () => { it( 'should render the product link', () => { render( -
- - + +
+ + +
); expect( screen.queryByText( linkUrl ) ).toBeInTheDocument(); @@ -53,9 +72,11 @@ describe( 'ProductDetailsSection', () => { it( 'should hide the product link if field name has errors', () => { render( -
- - + +
+ + +
); userEvent.clear( screen.getByLabelText( 'Name' ) ); userEvent.tab(); diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index dd9131c0f09..3a602997d23 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -50,12 +50,14 @@ "@automattic/explat-client-react-helpers": "^0.0.4", "@automattic/interpolate-components": "^1.2.0", "@react-spring/web": "^9.4.3", + "@types/wordpress__blocks": "^11.0.7", "@woocommerce/api": "^0.2.0", "@woocommerce/e2e-environment": "^0.3.0", "@woocommerce/e2e-utils": "^0.2.0", "@wordpress/a11y": "^3.5.0", "@wordpress/api-fetch": "^6.0.1", "@wordpress/base-styles": "^4.3.0", + "@wordpress/blocks": "^11.17.0", "@wordpress/components": "^19.5.0", "@wordpress/compose": "^5.1.2", "@wordpress/core-data": "^4.1.2", @@ -118,6 +120,7 @@ "@types/jest": "^27.4.1", "@types/lodash": "^4.14.179", "@types/puppeteer": "^4.0.2", + "@types/qs": "^6.9.7", "@types/react": "^17.0.0", "@types/react-router-dom": "^5.3.3", "@types/react-transition-group": "^4.4.4", @@ -129,7 +132,6 @@ "@types/wordpress__media-utils": "^3.0.0", "@types/wordpress__notices": "^3.3.0", "@types/wordpress__plugins": "^3.0.0", - "@types/qs": "^6.9.7", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "@woocommerce/admin-e2e-tests": "workspace:*", @@ -151,6 +153,7 @@ "@woocommerce/onboarding": "workspace:*", "@woocommerce/tracks": "workspace:*", "@wordpress/babel-preset-default": "^6.5.1", + "@wordpress/block-editor": "^9.8.0", "@wordpress/browserslist-config": "^4.1.1", "@wordpress/custom-templated-path-webpack-plugin": "^2.1.2", "@wordpress/jest-preset-default": "^8.0.1", diff --git a/plugins/woocommerce/changelog/add-product-management-description b/plugins/woocommerce/changelog/add-product-management-description new file mode 100644 index 00000000000..97acec67012 --- /dev/null +++ b/plugins/woocommerce/changelog/add-product-management-description @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product management description to new product management experience diff --git a/plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php b/plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php new file mode 100644 index 00000000000..f0bcd4da1d3 --- /dev/null +++ b/plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php @@ -0,0 +1,37 @@ +=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.17.7 - '@babel/core': 7.12.9 - '@babel/helper-validator-option': 7.16.7 - browserslist: 4.20.4 - semver: 6.3.0 - dev: true - - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.16.12: - resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.17.7 - '@babel/core': 7.16.12 - '@babel/helper-validator-option': 7.16.7 - browserslist: 4.20.4 - semver: 6.3.0 - dev: false - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.17.8: resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} engines: {node: '>=6.9.0'} @@ -2826,6 +2806,29 @@ packages: '@babel/core': 7.17.8 '@babel/helper-annotate-as-pure': 7.18.6 regexpu-core: 5.2.1 + dev: true + + /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.12.9: + resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.2.1 + dev: true + + /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.16.12: + resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.2.1 + dev: false /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.17.8: resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} @@ -3155,6 +3158,7 @@ packages: /@babel/helper-plugin-utils/7.14.5: resolution: {integrity: sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-plugin-utils/7.18.9: resolution: {integrity: sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==} @@ -3292,6 +3296,7 @@ packages: /@babel/helper-validator-identifier/7.15.7: resolution: {integrity: sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-validator-identifier/7.16.7: resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} @@ -3484,7 +3489,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-remap-async-to-generator': 7.16.8 '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.12.9 transitivePeerDependencies: @@ -3498,7 +3503,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-remap-async-to-generator': 7.16.8 '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.16.12 transitivePeerDependencies: @@ -3541,7 +3546,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -3554,7 +3559,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -3567,7 +3572,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: false @@ -3604,7 +3609,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.12.9 transitivePeerDependencies: - supports-color @@ -3618,7 +3623,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -3672,7 +3677,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 dev: true @@ -3736,7 +3741,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 dev: true @@ -3790,7 +3795,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 dev: true @@ -3844,7 +3849,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 dev: true @@ -3898,7 +3903,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.12.9 dev: true @@ -3951,7 +3956,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 dev: true @@ -4029,12 +4034,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.12.9 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.12.9 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.12.9 dev: true /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.16.12: @@ -4043,12 +4048,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.16.12 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.16.12 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.16.12 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.16.12 dev: false /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.17.8: @@ -4085,7 +4090,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 dev: true @@ -4139,7 +4144,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.12.9 dev: true @@ -4222,7 +4227,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -4345,7 +4350,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.12.9: @@ -4379,6 +4384,29 @@ packages: '@babel/core': 7.17.8 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.17.8 '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} @@ -4502,7 +4530,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.16.12: @@ -4511,7 +4539,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.17.8: @@ -4520,7 +4548,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-export-default-from/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-4C3E4NsrLOgftKaTYTULhHsuQrGv3FHrBzOMDiS7UYKIpgGBkAdawg4h+EI8zPeK9M0fiIIh72hIwsI24K7MbA==} @@ -4598,7 +4626,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.16.12: resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -4606,7 +4634,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.17.8: @@ -4615,7 +4643,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-jsx/7.12.1_@babel+core@7.12.9: resolution: {integrity: sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==} @@ -4633,7 +4661,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-syntax-jsx/7.16.0_@babel+core@7.16.12: @@ -4643,7 +4671,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-jsx/7.16.7_@babel+core@7.17.8: @@ -4721,7 +4749,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.16.12: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -4729,7 +4757,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.17.8: @@ -4738,7 +4766,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.12.9: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4746,7 +4774,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.16.12: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4754,7 +4782,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.17.8: @@ -4763,7 +4791,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.12.9: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -4771,7 +4799,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.16.12: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -4779,7 +4807,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.17.8: @@ -4788,7 +4816,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.12.9: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -4851,7 +4879,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.16.12: resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -4860,7 +4888,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.17.8: @@ -4870,7 +4898,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-typescript/7.16.7_@babel+core@7.16.12: resolution: {integrity: sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==} @@ -4907,7 +4935,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.12.9: @@ -4957,7 +4985,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-remap-async-to-generator': 7.16.4 transitivePeerDependencies: - supports-color @@ -5025,7 +5053,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.12.9: @@ -5074,7 +5102,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.12.9: @@ -5126,7 +5154,7 @@ packages: '@babel/helper-annotate-as-pure': 7.16.0 '@babel/helper-function-name': 7.16.0 '@babel/helper-optimise-call-expression': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.0 '@babel/helper-split-export-declaration': 7.16.0 globals: 11.12.0 @@ -5145,7 +5173,7 @@ packages: '@babel/helper-environment-visitor': 7.16.7 '@babel/helper-function-name': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.7 '@babel/helper-split-export-declaration': 7.16.7 globals: 11.12.0 @@ -5164,7 +5192,7 @@ packages: '@babel/helper-environment-visitor': 7.16.7 '@babel/helper-function-name': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.7 '@babel/helper-split-export-declaration': 7.16.7 globals: 11.12.0 @@ -5217,7 +5245,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.12.9: @@ -5266,7 +5294,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.12.9: @@ -5316,7 +5344,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.12.9: @@ -5350,6 +5378,29 @@ packages: '@babel/core': 7.17.8 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.17.8 '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} @@ -5368,7 +5419,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.12.9: @@ -5418,7 +5469,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.12.9: @@ -5429,7 +5480,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.16.12: @@ -5440,7 +5491,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.17.8: @@ -5481,7 +5532,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.12.9: @@ -5531,7 +5582,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-function-name': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.12.9: @@ -5543,7 +5594,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 '@babel/helper-function-name': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.16.12: @@ -5555,7 +5606,7 @@ packages: '@babel/core': 7.16.12 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 '@babel/helper-function-name': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.17.8: @@ -5588,7 +5639,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-literals/7.16.7_@babel+core@7.12.9: @@ -5637,7 +5688,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.12.9: @@ -5687,7 +5738,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: - supports-color @@ -5756,7 +5807,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-simple-access': 7.16.0 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5771,7 +5822,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-simple-access': 7.17.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5786,7 +5837,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-simple-access': 7.17.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5830,7 +5881,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-hoist-variables': 7.16.0 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-validator-identifier': 7.15.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5846,7 +5897,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-hoist-variables': 7.16.7 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-validator-identifier': 7.16.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5862,7 +5913,7 @@ packages: '@babel/core': 7.16.12 '@babel/helper-hoist-variables': 7.16.7 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-validator-identifier': 7.16.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5908,7 +5959,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -6021,7 +6072,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.12.9: @@ -6070,7 +6121,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.0 transitivePeerDependencies: - supports-color @@ -6083,7 +6134,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.7 transitivePeerDependencies: - supports-color @@ -6096,7 +6147,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.7 transitivePeerDependencies: - supports-color @@ -6134,7 +6185,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.12.9: @@ -6177,6 +6228,16 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.16.12: + resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.17.8: resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} engines: {node: '>=6.9.0'} @@ -6193,7 +6254,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.12.9: @@ -6305,9 +6366,9 @@ packages: '@babel/core': 7.16.12 '@babel/helper-annotate-as-pure': 7.16.0 '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-jsx': 7.16.0_@babel+core@7.16.12 - '@babel/types': 7.16.0 + '@babel/types': 7.19.3 dev: false /@babel/plugin-transform-react-jsx/7.17.3_@babel+core@7.17.8: @@ -6405,7 +6466,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.12.9: @@ -6472,8 +6533,8 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 semver: 6.3.0 @@ -6489,8 +6550,8 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.17.8 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.17.8 semver: 6.3.0 @@ -6521,7 +6582,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.12.9: @@ -6570,7 +6631,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 dev: true @@ -6582,7 +6643,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 dev: true /@babel/plugin-transform-spread/7.16.7_@babel+core@7.16.12: @@ -6593,7 +6654,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 dev: false /@babel/plugin-transform-spread/7.16.7_@babel+core@7.17.8: @@ -6624,7 +6685,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.12.9: @@ -6673,7 +6734,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.12.9: @@ -6722,7 +6783,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.12.9: @@ -6811,7 +6872,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.12.9: @@ -6861,7 +6922,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.12.9: @@ -6872,7 +6933,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.16.12: @@ -6883,7 +6944,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.17.8: @@ -6999,7 +7060,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.12.9 - '@babel/helper-compilation-targets': 7.17.7_@babel+core@7.12.9 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-option': 7.16.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7_@babel+core@7.12.9 @@ -7066,7 +7127,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.12.9 '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.12.9 '@babel/preset-modules': 0.1.5_@babel+core@7.12.9 - '@babel/types': 7.17.0 + '@babel/types': 7.19.3 babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.12.9 babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.12.9 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.12.9 @@ -7084,7 +7145,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.16.12 - '@babel/helper-compilation-targets': 7.17.7_@babel+core@7.16.12 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-option': 7.16.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7_@babel+core@7.16.12 @@ -7151,7 +7212,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.16.12 '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.16.12 '@babel/preset-modules': 0.1.5_@babel+core@7.16.12 - '@babel/types': 7.17.0 + '@babel/types': 7.19.3 babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 @@ -7348,9 +7409,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.12.9 '@babel/types': 7.19.3 esutils: 2.0.3 dev: true @@ -7361,9 +7422,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.16.12 '@babel/types': 7.19.3 esutils: 2.0.3 dev: false @@ -7374,9 +7435,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.17.8 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.17.8 '@babel/types': 7.19.3 esutils: 2.0.3 @@ -7402,8 +7463,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-validator-option': 7.16.7 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-option': 7.18.6 '@babel/plugin-transform-typescript': 7.16.8_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -7536,6 +7597,7 @@ packages: dependencies: '@babel/helper-validator-identifier': 7.15.7 to-fast-properties: 2.0.0 + dev: true /@babel/types/7.17.0: resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} @@ -7590,7 +7652,6 @@ packages: /@discoveryjs/json-ext/0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - dev: true /@emotion/babel-plugin/11.7.2_@babel+core@7.17.8: resolution: {integrity: sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ==} @@ -7610,7 +7671,6 @@ packages: find-root: 1.1.0 source-map: 0.5.7 stylis: 4.0.13 - dev: false /@emotion/cache/10.0.29: resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==} @@ -7628,7 +7688,6 @@ packages: '@emotion/utils': 1.1.0 '@emotion/weak-memoize': 0.2.5 stylis: 4.0.13 - dev: false /@emotion/core/10.3.1_react@16.14.0: resolution: {integrity: sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==} @@ -7679,13 +7738,13 @@ packages: '@emotion/serialize': 1.0.2 '@emotion/sheet': 1.1.0 '@emotion/utils': 1.1.0 - dev: false /@emotion/hash/0.8.0: resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} /@emotion/is-prop-valid/0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true dependencies: '@emotion/memoize': 0.7.4 @@ -7693,14 +7752,12 @@ packages: resolution: {integrity: sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==} dependencies: '@emotion/memoize': 0.7.5 - dev: false /@emotion/memoize/0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} /@emotion/memoize/0.7.5: resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==} - dev: false /@emotion/native/10.0.27_peeplpcor766cv2dor4ihhuuki: resolution: {integrity: sha512-3qxR2XFizGfABKKbX9kAYc0PHhKuCEuyxshoq3TaMEbi9asWHdQVChg32ULpblm4XAf9oxaitAU7J9SfdwFxtw==} @@ -7770,7 +7827,6 @@ packages: '@types/react': 17.0.40 hoist-non-react-statics: 3.3.2 react: 17.0.2 - dev: false /@emotion/serialize/0.11.16: resolution: {integrity: sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==} @@ -7789,14 +7845,12 @@ packages: '@emotion/unitless': 0.7.5 '@emotion/utils': 1.1.0 csstype: 3.0.10 - dev: false /@emotion/sheet/0.9.4: resolution: {integrity: sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==} /@emotion/sheet/1.1.0: resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==} - dev: false /@emotion/styled-base/10.3.0_gfrer23gq2rp2t523t6qbxrx6m: resolution: {integrity: sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==} @@ -7872,7 +7926,6 @@ packages: '@emotion/utils': 1.1.0 '@types/react': 17.0.40 react: 17.0.2 - dev: false /@emotion/styled/11.8.1_lddnk6nv2rrayprsm6yu5n7lz4: resolution: {integrity: sha512-OghEVAYBZMpEquHZwuelXcRjRJQOVayvbmNR0zr174NHdmMgrNkLC6TljKC5h9lZLkN5WGrdUcrKlOJ4phhoTQ==} @@ -7912,7 +7965,6 @@ packages: /@emotion/utils/1.1.0: resolution: {integrity: sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==} - dev: false /@emotion/weak-memoize/0.2.5: resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==} @@ -8015,7 +8067,6 @@ packages: /@floating-ui/core/1.0.1: resolution: {integrity: sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==} - dev: false /@floating-ui/dom/0.4.5: resolution: {integrity: sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==} @@ -8027,7 +8078,6 @@ packages: resolution: {integrity: sha512-5X9WSvZ8/fjy3gDu8yx9HAA4KG1lazUN2P4/VnaXLxTO9Dz53HI1oYoh1OlhqFNlHgGDiwFX5WhFCc2ljbW3yA==} dependencies: '@floating-ui/core': 1.0.1 - dev: false /@floating-ui/react-dom/0.6.3_6rln7q2jvtuewdvbdwpg4txtvm: resolution: {integrity: sha512-hC+pS5D6AgS2wWjbmSQ6UR6Kpy+drvWGJIri6e1EDGADTPsCaa4KzCgmCczHrQeInx9tqs81EyDmbKJYY2swKg==} @@ -8052,7 +8102,6 @@ packages: '@floating-ui/dom': 1.0.2 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /@gar/promisify/1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -9880,7 +9929,6 @@ packages: '@react-spring/shared': 9.5.5_react@17.0.2 '@react-spring/types': 9.5.5 react: 17.0.2 - dev: false /@react-spring/core/9.4.4_react@17.0.2: resolution: {integrity: sha512-llgb0ljFyjMB0JhWsaFHOi9XFT8n1jBMVs1IFY2ipIBerWIRWrgUmIpakLPHTa4c4jwqTaDSwX90s2a0iN7dxQ==} @@ -9904,7 +9952,6 @@ packages: '@react-spring/shared': 9.5.5_react@17.0.2 '@react-spring/types': 9.5.5 react: 17.0.2 - dev: false /@react-spring/rafz/9.4.4: resolution: {integrity: sha512-5ki/sQ06Mdf8AuFstSt5zbNNicRT4LZogiJttDAww1ozhuvemafNWEHxhzcULgCPCDu2s7HsroaISV7+GQWrhw==} @@ -9912,7 +9959,6 @@ packages: /@react-spring/rafz/9.5.5: resolution: {integrity: sha512-F/CLwB0d10jL6My5vgzRQxCNY2RNyDJZedRBK7FsngdCmzoq3V4OqqNc/9voJb9qRC2wd55oGXUeXv2eIaFmsw==} - dev: false /@react-spring/shared/9.4.4_react@17.0.2: resolution: {integrity: sha512-ySVgScDZlhm/+Iy2smY9i/DDrShArY0j6zjTS/Re1lasKnhq8qigoGiAxe8xMPJNlCaj3uczCqHy3TY9bKRtfQ==} @@ -9932,7 +9978,6 @@ packages: '@react-spring/rafz': 9.5.5 '@react-spring/types': 9.5.5 react: 17.0.2 - dev: false /@react-spring/types/9.4.4: resolution: {integrity: sha512-KpxKt/D//q/t/6FBcde/RE36LKp8PpWu7kFEMLwpzMGl9RpcexunmYOQJWwmJWtkQjgE1YRr7DzBMryz6La1cQ==} @@ -9940,7 +9985,6 @@ packages: /@react-spring/types/9.5.5: resolution: {integrity: sha512-7I/qY8H7Enwasxr4jU6WmtNK+RZ4Z/XvSlDvjXFVe7ii1x0MoSlkw6pD7xuac8qrHQRm9BTcbZNyeeKApYsvCg==} - dev: false /@react-spring/web/9.4.4_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-iJmOLdhcuizriUlu/xqBc5y8KaFts+UI+iC+GxyTwBtzxA9czKiSAZW2ESuhG8stafa3jncwjfTQQp84KN36cw==} @@ -9968,7 +10012,6 @@ packages: '@react-spring/types': 9.5.5 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /@romainberger/css-diff/1.0.3: resolution: {integrity: sha512-zR2EvxtJvQXRxFtTnqazMsJADngyVIulzYQ+wVYWRC1Hw3e4gfEIbigX46wTsPUyjAI+lRXFrBSoCWcgZ6ZSlQ==} @@ -13034,7 +13077,7 @@ packages: dependencies: '@babel/core': 7.17.8 postcss: 7.0.39 - postcss-syntax: 0.36.2_postcss@7.0.39 + postcss-syntax: 0.36.2_postcss@8.4.12 transitivePeerDependencies: - supports-color dev: true @@ -13047,7 +13090,7 @@ packages: postcss-syntax: '>=0.36.2' dependencies: postcss: 7.0.39 - postcss-syntax: 0.36.2_postcss@7.0.39 + postcss-syntax: 0.36.2_postcss@8.4.12 remark: 13.0.0 unist-util-find-all-after: 3.0.2 transitivePeerDependencies: @@ -14038,12 +14081,12 @@ packages: '@typescript-eslint/scope-manager': 5.15.0 '@typescript-eslint/type-utils': 5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q '@typescript-eslint/utils': 5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q - debug: 4.3.4 + debug: 4.3.3 eslint: 8.25.0 functional-red-black-tree: 1.0.1 ignore: 5.2.0 regexpp: 3.2.0 - semver: 7.3.7 + semver: 7.3.5 tsutils: 3.21.0_typescript@4.8.4 typescript: 4.8.4 transitivePeerDependencies: @@ -14064,12 +14107,12 @@ packages: '@typescript-eslint/scope-manager': 5.15.0 '@typescript-eslint/type-utils': 5.15.0_himlt4eddny2rsb5zkuydvuf7u '@typescript-eslint/utils': 5.15.0_himlt4eddny2rsb5zkuydvuf7u - debug: 4.3.4 + debug: 4.3.3 eslint: 8.11.0 functional-red-black-tree: 1.0.1 ignore: 5.2.0 regexpp: 3.2.0 - semver: 7.3.7 + semver: 7.3.5 tsutils: 3.21.0_typescript@4.8.4 typescript: 4.8.4 transitivePeerDependencies: @@ -14757,7 +14800,6 @@ packages: /@use-gesture/core/10.2.10: resolution: {integrity: sha512-7WFIDfeTB+7RBui8YOrB2xbgmvMsvaCDjyzrdvECKkgOpIynNSdhlLXjiFuqQMtnK71IL/9WNZNU0P8xuaLuUQ==} - dev: false /@use-gesture/react/10.2.10_react@17.0.2: resolution: {integrity: sha512-znChnKVAMMGXD9J7fCKN686BJNBlUJaRtCu92IQXVWdcxg4MqS0SgsBslGnTWXTlsHVkg5zcGjKYf7qYkOf0Rg==} @@ -14766,7 +14808,6 @@ packages: dependencies: '@use-gesture/core': 10.2.10 react: 17.0.2 - dev: false /@webassemblyjs/ast/1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} @@ -14994,7 +15035,6 @@ packages: dependencies: webpack: 5.70.0_webpack-cli@4.9.2 webpack-cli: 4.9.2_wbg6qaiqcwsayvtung7xs6mhka - dev: true /@webpack-cli/info/1.4.1_webpack-cli@4.9.2: resolution: {integrity: sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==} @@ -15003,7 +15043,6 @@ packages: dependencies: envinfo: 7.8.1 webpack-cli: 4.9.2_wbg6qaiqcwsayvtung7xs6mhka - dev: true /@webpack-cli/serve/1.6.1_webpack-cli@4.9.2: resolution: {integrity: sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==} @@ -15015,7 +15054,6 @@ packages: optional: true dependencies: webpack-cli: 4.9.2_wbg6qaiqcwsayvtung7xs6mhka - dev: true /@wojtekmaj/enzyme-adapter-react-17/0.6.6_7ltvq4e2railvf5uya4ffxpe2a: resolution: {integrity: sha512-gSfhg8CiL0Vwc2UgUblGVZIy7M0KyXaZsd8+QwzV8TSVRLkGyzdLtYEcs9wRWyQTsdmOd+oRGqbVgUX7AVJxug==} @@ -15120,7 +15158,6 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/dom-ready': 3.19.0 '@wordpress/i18n': 4.19.0 - dev: false /@wordpress/a11y/3.4.1: resolution: {integrity: sha512-SjeLO8x/Y/QAcKBrvyJiu8KVAPckRLNwuFfgX7zCGM8vBfg+Depj94Hp55ARLjq0oXHg7EWKxSdzNkvmTz8AIA==} @@ -15192,14 +15229,12 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/i18n': 4.19.0 '@wordpress/url': 3.20.0 - dev: false /@wordpress/autop/3.19.0: resolution: {integrity: sha512-Vl164Ilwmkx3M0LEyXkFdgksHjs3/FnHtw76tvdjjnLXtErUUIZ2y+hdCe+Esh8BhAUYXW420JU5KKvbidmykg==} engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/babel-plugin-import-jsx-pragma/1.1.3_@babel+core@7.12.9: resolution: {integrity: sha512-WkVeFZpM5yuHigWe8llZDeMRa4bhMQoHu9dzs1s3cmB1do2mhk341Iw34FidWto14Dzd+383K71vxJejqjKOwQ==} @@ -15408,7 +15443,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/block-editor/10.2.0_67fiyx7k2wr2ple2yfldahug5u: resolution: {integrity: sha512-9Bxq9hY3WEqodn/K/WSE+PoIwv6jKkKBP0pxXFJTWV1yc8/Np9QHV/7wG7qjztxxgu00FrYF7u8OZyvjPrSNYw==} @@ -15482,19 +15516,19 @@ packages: '@wordpress/blob': 3.19.0 '@wordpress/blocks': 11.18.0_react@17.0.2 '@wordpress/components': 20.0.0_67fiyx7k2wr2ple2yfldahug5u - '@wordpress/compose': 5.14.0_react@17.0.2 + '@wordpress/compose': 5.17.0_react@17.0.2 '@wordpress/data': 7.3.0_react@17.0.2 '@wordpress/date': 4.19.0 - '@wordpress/deprecated': 3.16.0 - '@wordpress/dom': 3.16.0 + '@wordpress/deprecated': 3.19.0 + '@wordpress/dom': 3.19.0 '@wordpress/element': 4.17.0 - '@wordpress/hooks': 3.16.0 + '@wordpress/hooks': 3.19.0 '@wordpress/html-entities': 3.19.0 - '@wordpress/i18n': 4.16.0 + '@wordpress/i18n': 4.19.0 '@wordpress/icons': 9.10.0 - '@wordpress/is-shallow-equal': 4.16.0 + '@wordpress/is-shallow-equal': 4.19.0 '@wordpress/keyboard-shortcuts': 3.17.0_react@17.0.2 - '@wordpress/keycodes': 3.16.0 + '@wordpress/keycodes': 3.19.0 '@wordpress/notices': 3.19.0_react@17.0.2 '@wordpress/rich-text': 5.17.0_react@17.0.2 '@wordpress/shortcode': 3.19.0 @@ -15520,7 +15554,6 @@ packages: transitivePeerDependencies: - '@babel/core' - '@types/react' - dev: false /@wordpress/block-library/7.16.0_67fiyx7k2wr2ple2yfldahug5u: resolution: {integrity: sha512-iuFqo2Ms08z0s1t1MM4mI7Gt+oBmj7KW6hRPEdQst+8jaG6hpQX6TgOzBt2Nw+0P0w8QRdyJjoQsB1cipGcNgQ==} @@ -15581,7 +15614,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/blocks/11.18.0_react@17.0.2: resolution: {integrity: sha512-6YHyDQNa6UrAzF3oKFOyu/1F32u7h5q/gpsE1439KDGVLsrc8rSxx3rE6G6TXbJ5YC8MqDrOItMwbw14TGKPAQ==} @@ -15615,7 +15647,6 @@ packages: showdown: 1.9.1 simple-html-tokenizer: 0.5.11 uuid: 8.3.2 - dev: false /@wordpress/blocks/11.3.1_react@17.0.2: resolution: {integrity: sha512-0T/qD1/hxJpNrUrJ2suZY0MP6Gw83mXfkaOupZ7rwjcWEi8c6AmzXaU/amAMNobM6oiNr4Sa6FctnnTGCEC1mQ==} @@ -15806,7 +15837,7 @@ packages: '@wordpress/is-shallow-equal': 4.4.1 '@wordpress/keycodes': 3.4.1 '@wordpress/primitives': 3.2.1 - '@wordpress/rich-text': 5.17.0_react@17.0.2 + '@wordpress/rich-text': 5.2.1_react@17.0.2 '@wordpress/warning': 2.4.1 classnames: 2.3.1 colord: 2.9.2 @@ -15860,7 +15891,7 @@ packages: '@wordpress/is-shallow-equal': 4.4.1 '@wordpress/keycodes': 3.4.1 '@wordpress/primitives': 3.2.1 - '@wordpress/rich-text': 5.17.0_react@17.0.2 + '@wordpress/rich-text': 5.2.1_react@17.0.2 '@wordpress/warning': 2.4.1 classnames: 2.3.1 colord: 2.9.2 @@ -15974,7 +16005,7 @@ packages: change-case: 4.1.2 classnames: 2.3.1 colord: 2.9.2 - date-fns: 2.28.0 + date-fns: 2.29.3 dom-scroll-into-view: 1.2.1 downshift: 6.1.9_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m @@ -15993,7 +16024,6 @@ packages: transitivePeerDependencies: - '@babel/core' - '@types/react' - dev: false /@wordpress/components/21.2.0_67fiyx7k2wr2ple2yfldahug5u: resolution: {integrity: sha512-pYz+EY+Tv/O2JuDBXpaFH/zv9Evty/e6NOGjOzddSeaShZ/mCq2DpUSWPuTFBEAjtv6h9HnpkakbNnEeio5yNA==} @@ -16135,7 +16165,6 @@ packages: mousetrap: 1.6.5 react: 17.0.2 use-memo-one: 1.1.2_react@17.0.2 - dev: false /@wordpress/compose/5.2.1_react@17.0.2: resolution: {integrity: sha512-0l5UOiq5tDFeuIsdSVsWzNETHZagTnSBSTdGsxDmKIi5NC7vf1pXs4rlrEA45vUdFm/SbpIA9gp+NFzfpVKIXw==} @@ -16377,7 +16406,6 @@ packages: redux: 4.2.0 turbo-combine-reducers: 1.0.2 use-memo-one: 1.1.2_react@17.0.2 - dev: false /@wordpress/date/3.15.1: resolution: {integrity: sha512-SuHiObvjbegL8RpaSQ6JqFnG+QyGP+oUhx1FZDMdt1nOQA9HE7D5ssVlZFlMEAdo6iS8xMuW+4SgJN3Eo1fb4w==} @@ -16395,7 +16423,6 @@ packages: '@wordpress/deprecated': 3.19.0 moment: 2.29.1 moment-timezone: 0.5.34 - dev: false /@wordpress/date/4.4.1: resolution: {integrity: sha512-G2qcMB+EekBLIMO0YTEvhSfhTDAGM94WGe696DG4EevlBmMmgTSCATR8IvlD2+rta5Ut8qPJ1w2i0cy4AwslJA==} @@ -16480,7 +16507,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/dom-ready/3.4.1: resolution: {integrity: sha512-w6DVKKpNwX0XUp0Cuh1OyFyGXLabr47k/ecRHKmQkQh9LdjRew7QvxUHYDN1rejRvq5GqcDb7Gnkz4E6hWIo4Q==} @@ -16504,14 +16530,6 @@ packages: lodash: 4.17.21 dev: false - /@wordpress/dom/3.16.0: - resolution: {integrity: sha512-WOwEYXQWaZ4ZkQgL//jyB/FN33vPuFUHcr1Tc0o1T5zScNJrWVTiILokkFVv2AxqPZkrq4WhxKN9ZGRyo6VlOA==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - '@wordpress/deprecated': 3.19.0 - dev: false - /@wordpress/dom/3.19.0: resolution: {integrity: sha512-re4o53E5w0c5j9IW5vw8daTTmx6JQ9xgwxy2uWddAgSau7jq4LvvlWORCmgFEiCcFGgRBxhqJTndquKuDJ34PQ==} engines: {node: '>=12'} @@ -16572,8 +16590,8 @@ packages: puppeteer: '>=1.19.0' dependencies: '@babel/runtime': 7.19.0 - '@wordpress/keycodes': 3.16.0 - '@wordpress/url': 3.16.0 + '@wordpress/keycodes': 3.19.0 + '@wordpress/url': 3.20.0 jest: 27.5.1 lodash: 4.17.21 node-fetch: 2.6.7 @@ -16585,7 +16603,7 @@ packages: /@wordpress/element/2.20.3: resolution: {integrity: sha512-f4ZPTDf9CxiiOXiMxc4v1K7jcBMT4dsiehVOpkKzCDKboNXp4qVf8oe5PE23VGZNEjcOj5Mkg9hB57R0nqvMTw==} dependencies: - '@babel/runtime': 7.17.7 + '@babel/runtime': 7.19.0 '@types/react': 16.14.31 '@types/react-dom': 16.9.16 '@wordpress/escape-html': 1.12.2 @@ -16836,13 +16854,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 - /@wordpress/hooks/3.16.0: - resolution: {integrity: sha512-KpY8KFp2/3TX6lKmffNmdkeaH9c4CN1iJ8SiCufjGgRCnVWmWe/HcEJ5OjhUvBnRkhsLMY7pvlXMU8Mh7nLxyA==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - dev: false - /@wordpress/hooks/3.19.0: resolution: {integrity: sha512-iJNZnQ08ZFFlXpVBbSA2NuVMiKxGpNLQsDiwIuIzTmwWl8ZECYikuGC3vMCiG3xUz47JR5eGA5wN63kjkPm5bA==} engines: {node: '>=12'} @@ -16875,7 +16886,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/html-entities/3.4.1: resolution: {integrity: sha512-wSuwgONTefnhCB9B7mKS+e8islHuCkprfDc+FhqVAa6r5RbVBGvaHUJs8embgdtww7MwBRMnskNf/buQ8Jr02A==} @@ -16924,20 +16934,6 @@ packages: tannin: 1.2.0 dev: false - /@wordpress/i18n/4.16.0: - resolution: {integrity: sha512-N7BChVVaQpt63e2Wgc0ST+ahUuhSjd6bqHqgIBnxZ4LU3c8tzd/etYjBqSM8RPcI9gSOM32ddlTnJgAxgntKaA==} - engines: {node: '>=12'} - hasBin: true - dependencies: - '@babel/runtime': 7.19.0 - '@wordpress/hooks': 3.19.0 - gettext-parser: 1.4.0 - lodash: 4.17.21 - memize: 1.1.0 - sprintf-js: 1.1.2 - tannin: 1.2.0 - dev: false - /@wordpress/i18n/4.19.0: resolution: {integrity: sha512-FL2+NghSYLqd9iib8otQLYF/G7EHh+/9zKbW6K3ok+FNwpF7/8UaMq/52CED2zWqD36Uw0vNX9AP7gRYTot0cA==} engines: {node: '>=12'} @@ -17020,7 +17016,6 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/element': 4.17.0 '@wordpress/primitives': 3.17.0 - dev: false /@wordpress/is-shallow-equal/3.1.3: resolution: {integrity: sha512-eDLhfC4aaSgklzqwc6F/F4zmJVpTVTAvhqX+q0SP/8LPcP2HuKErPHVrEc75PMWqIutja2wJg98YSNPdewrj1w==} @@ -17185,7 +17180,6 @@ packages: '@wordpress/keycodes': 3.19.0 react: 17.0.2 rememo: 4.0.0 - dev: false /@wordpress/keycodes/2.19.3: resolution: {integrity: sha512-8rNdmP5M1ifTgLIL0dt/N1uTGsq/Rx1ydCXy+gg24WdxBRhyu5sudNVCtascVXo26aIfOH9OJRdqRZZTEORhog==} @@ -17194,16 +17188,6 @@ packages: '@wordpress/i18n': 3.20.0 lodash: 4.17.21 - /@wordpress/keycodes/3.16.0: - resolution: {integrity: sha512-Vs/t3GBMaJ3dBAPZfhuZBuxdwagJdXhpSpvnkX3/MJrn6sRrLKijxkWK8x26PfkDePQ+3kiupP2pEoIwSCTUXg==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - '@wordpress/i18n': 4.19.0 - change-case: 4.1.2 - lodash: 4.17.21 - dev: false - /@wordpress/keycodes/3.19.0: resolution: {integrity: sha512-uEITYKlknuZPP9tSF0y8s/GECsgJMceUkfFJH3JplKpPvw5RYJB49hTg40P5CPVoRLJgvJqoeR1Bdo3o312wjA==} engines: {node: '>=12'} @@ -17243,7 +17227,6 @@ packages: '@wordpress/a11y': 3.19.0 '@wordpress/data': 7.3.0_react@17.0.2 react: 17.0.2 - dev: false /@wordpress/notices/3.4.1_react@17.0.2: resolution: {integrity: sha512-Y7e2GLlB5wjLOtxsXzJd3jg/p6LV2GeeUnk+reURqUbb/4rlVlXQuMPOboTxLRB/0eTMNwWFI/MIr+NKbuY7MQ==} @@ -17368,7 +17351,6 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/element': 4.17.0 classnames: 2.3.1 - dev: false /@wordpress/primitives/3.2.1: resolution: {integrity: sha512-dOrQQudydRw4szT60t+5b9jwMwxB4LMxNRlkbyGqqNwjv11Vq52FT9rVeLs0CvlqklluCyZu5KnUp/dELxIYJw==} @@ -17414,7 +17396,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 requestidlecallback: 0.3.0 - dev: false /@wordpress/priority-queue/2.4.1: resolution: {integrity: sha512-5+pyUvQCQTTkoiccnO5G6AUDxzCKdAiDh3oLbl+qLz3j56iGuLoKWR6L5ySj+knaYIZb4g8expFsbvf2+RcVtw==} @@ -17465,7 +17446,6 @@ packages: is-promise: 4.0.0 redux: 4.2.0 rungen: 0.3.2 - dev: false /@wordpress/redux-routine/4.4.1_redux@4.2.0: resolution: {integrity: sha512-AqSEWN0PNxp00g1da+laL2rr0SP0AAfGpoqfzd55wIjWMQnHEf2pDsLvo6gQ9jyauuY5Wn2GUsYmGjQ+WjSf4w==} @@ -17546,7 +17526,6 @@ packages: memize: 1.1.0 react: 17.0.2 rememo: 4.0.0 - dev: false /@wordpress/rich-text/5.2.1_react@17.0.2: resolution: {integrity: sha512-PBoDPQjihEOteHlDvVRtAjmDTx3T3NRr/GAX8MKVajECWFhiS6tKY2R/llg7fnJAinCIhEAfpNwQDpx2UCp3bA==} @@ -17754,7 +17733,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 memize: 1.1.0 - dev: false /@wordpress/style-engine/0.15.0: resolution: {integrity: sha512-F6wt4g8xnli6bOR0Syd4iz4r5jFha7DZLzi2krmgH3cSTK4DDPj2g1YOJrRIEqXX4aPmdZDurTqQZoJvt9qaqQ==} @@ -17762,7 +17740,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 lodash: 4.17.21 - dev: false /@wordpress/style-engine/1.2.0: resolution: {integrity: sha512-RoyTFpxDS7uOJuNG31J/153JLKCNftU1/wMMkf0qXDpP+1k4h9em1+iIPPAGPRW5pSq/ky95fAaQAnl+FgI6Wg==} @@ -17814,7 +17791,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/url/2.22.2_react-native@0.70.0: resolution: {integrity: sha512-aqpYKQXzyzkCOm+GzZRYlLb+wh58g0cwR1PaKAl0UXaBS4mdS+X6biMriylb4P8CVC/RR7CSw5XI20JC24KDwQ==} @@ -17825,14 +17801,6 @@ packages: transitivePeerDependencies: - react-native - /@wordpress/url/3.16.0: - resolution: {integrity: sha512-5hlT8KfioKrmfqQAHihj2pWqc8oMUFNae3n5/Wlu8H60Btf5h+cBfxr6eiOXPEVX9Ko9NskLjmAqCxxoiNviqg==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - remove-accents: 0.4.2 - dev: false - /@wordpress/url/3.20.0: resolution: {integrity: sha512-geLgg7AWh/pIFPQEI43hQYR6MvEUi3QIawaPPoYMqw3Fne1UvbnXW9ETsCLzYyegmV8DXzcYSMPeSBQf+HL/ig==} engines: {node: '>=12'} @@ -17895,7 +17863,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wp-g2/components/0.0.140_uk23rajygp47dvnd4kbkngbaoi: resolution: {integrity: sha512-bychuhZ3wPSB457CHYcogoPQPlP/eUA9GoTo0Fv0rj7f44Gr9XlPoqVT+GQa3CmPnvSCAl1sjoe75Vkaoo/O1w==} @@ -19209,7 +19176,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.12.9 '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.12.9 semver: 6.3.0 @@ -19222,7 +19189,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.16.12 '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.16.12 semver: 6.3.0 @@ -19243,6 +19210,19 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.16.12: + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.19.3 + '@babel/core': 7.16.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.16.12 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.17.8: resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -19274,7 +19254,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.12.9 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color dev: true @@ -19286,7 +19266,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.16.12 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color dev: false @@ -19298,7 +19278,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.17.8 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color dev: true @@ -19704,7 +19684,6 @@ packages: check-types: 8.0.3 hoopy: 0.1.4 tryer: 1.0.1 - dev: true /big-integer/1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} @@ -19789,7 +19768,6 @@ packages: /body-scroll-lock/3.1.5: resolution: {integrity: sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==} - dev: false /body/5.1.0: resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} @@ -20156,7 +20134,7 @@ packages: minipass-pipeline: 1.2.4 mkdirp: 1.0.4 p-map: 4.0.0 - promise-inflight: 1.0.1 + promise-inflight: 1.0.1_bluebird@3.7.2 rimraf: 3.0.2 ssri: 8.0.1 tar: 6.1.11 @@ -20534,7 +20512,6 @@ packages: /check-types/8.0.3: resolution: {integrity: sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==} - dev: true /cheerio-select/1.5.0: resolution: {integrity: sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==} @@ -20671,7 +20648,6 @@ packages: /classnames/2.3.1: resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} - dev: false /clean-css/4.2.4: resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} @@ -20984,7 +20960,6 @@ packages: /colorette/2.0.16: resolution: {integrity: sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==} - dev: true /colors/0.6.2: resolution: {integrity: sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==} @@ -21067,7 +21042,6 @@ packages: /commander/7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - dev: true /commander/8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} @@ -21311,7 +21285,7 @@ packages: normalize-path: 3.0.0 schema-utils: 4.0.0 serialize-javascript: 6.0.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 dev: true /core-js-compat/3.19.1: @@ -21651,7 +21625,7 @@ packages: postcss-modules-values: 4.0.0_postcss@8.4.12 postcss-value-parser: 4.2.0 semver: 7.3.5 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 dev: true /css-select-base-adapter/0.1.1: @@ -21986,7 +21960,6 @@ packages: /date-fns/2.29.3: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} - dev: false /dateformat/3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} @@ -22479,11 +22452,10 @@ packages: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: '@babel/runtime': 7.19.0 - csstype: 3.0.10 + csstype: 3.1.1 /dom-scroll-into-view/1.2.1: resolution: {integrity: sha512-LwNVg3GJOprWDO+QhLL1Z9MMgWe/KAFLxVWKzjRTxNSPn8/LLDIfmuG71YHznXCqaqTjvHJDYO1MEAgX6XCNbQ==} - dev: false /dom-serializer/0.2.2: resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} @@ -22632,7 +22604,6 @@ packages: /duplexer/0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - dev: true /duplexer3/0.1.4: resolution: {integrity: sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==} @@ -22664,7 +22635,6 @@ packages: resolution: {integrity: sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==} engines: {node: '>=0.10.0'} requiresBuild: true - dev: true /ejs/3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} @@ -24774,7 +24744,6 @@ packages: /fastest-levenshtein/1.0.12: resolution: {integrity: sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==} - dev: true /fastq/1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} @@ -24918,7 +24887,6 @@ packages: /filesize/3.6.1: resolution: {integrity: sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==} engines: {node: '>= 0.4.0'} - dev: true /fill-range/4.0.0: resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} @@ -25523,7 +25491,6 @@ packages: tslib: 2.3.1 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 - dev: false /framesync/4.1.0: resolution: {integrity: sha512-MmgZ4wCoeVxNbx2xp5hN/zPDCbLSKiDt4BbbslK7j/pM2lg5S0vhTNv1v8BCVb99JPIo6hXBFdwzU7Q4qcAaoQ==} @@ -25535,7 +25502,6 @@ packages: resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==} dependencies: tslib: 2.3.1 - dev: false /fresh/0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} @@ -26204,7 +26170,6 @@ packages: /gradient-parser/0.1.5: resolution: {integrity: sha1-DH4heVWeXOfY1x9EI6+TcQCyJIw=} engines: {node: '>=0.10.0'} - dev: false /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -26454,7 +26419,6 @@ packages: dependencies: duplexer: 0.1.2 pify: 4.0.1 - dev: true /gzip-size/6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} @@ -26694,11 +26658,9 @@ packages: /hey-listen/1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} - dev: false /highlight-words-core/1.2.2: resolution: {integrity: sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==} - dev: false /highlight.js/10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -26757,7 +26719,6 @@ packages: /hoopy/0.1.4: resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} engines: {node: '>= 6.0.0'} - dev: true /hosted-git-info/2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -26771,7 +26732,6 @@ packages: /hpq/1.3.0: resolution: {integrity: sha512-fvYTvdCFOWQupGxqkahrkA+ERBuMdzkxwtUdKrxR6rmMd4Pfl+iZ1QiQYoaZ0B/v0y59MOMnz3XFUWbT50/NWA==} - dev: false /hsl-regex/1.0.0: resolution: {integrity: sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=} @@ -27341,7 +27301,6 @@ packages: /interpret/2.2.0: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} - dev: true /invariant/2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -32212,7 +32171,7 @@ packages: webpack: ^5.0.0 dependencies: schema-utils: 4.0.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -32841,7 +32800,6 @@ packages: /normalize-wheel/1.0.1: resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} - dev: false /npm-bundled/1.1.2: resolution: {integrity: sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==} @@ -33252,7 +33210,6 @@ packages: /opener/1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - dev: true /opn/5.5.0: resolution: {integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==} @@ -33955,7 +33912,6 @@ packages: hey-listen: 1.0.8 style-value-types: 5.0.0 tslib: 2.3.1 - dev: false /popmotion/9.0.0-rc.20: resolution: {integrity: sha512-f98sny03WuA+c8ckBjNNXotJD4G2utG/I3Q23NU69OEafrXtxxSukAaJBxzbtxwDvz3vtZK69pu9ojdkMoBNTg==} @@ -34131,7 +34087,7 @@ packages: dependencies: htmlparser2: 3.10.1 postcss: 7.0.39 - postcss-syntax: 0.36.2_postcss@7.0.39 + postcss-syntax: 0.36.2_postcss@8.4.12 dev: true /postcss-less/3.1.4: @@ -34718,6 +34674,30 @@ packages: postcss: 7.0.39 dev: true + /postcss-syntax/0.36.2_postcss@8.4.12: + resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} + peerDependencies: + postcss: '>=5.0.0' + postcss-html: '*' + postcss-jsx: '*' + postcss-less: '*' + postcss-markdown: '*' + postcss-scss: '*' + peerDependenciesMeta: + postcss-html: + optional: true + postcss-jsx: + optional: true + postcss-less: + optional: true + postcss-markdown: + optional: true + postcss-scss: + optional: true + dependencies: + postcss: 8.4.12 + dev: true + /postcss-unique-selectors/4.0.1: resolution: {integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==} engines: {node: '>=6.9.0'} @@ -35695,7 +35675,6 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 tslib: 2.0.1 - dev: false /react-easy-crop/4.5.1_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-MVzCWmKXTwZTK0iYqlF/gPLdLqvUGrLGX7SQ4g+DO3b/lCiVAwxZKLeZ1wjDfG+r/yEWUoL7At5a0kkDJeU+rQ==} @@ -36264,24 +36243,6 @@ packages: react-dom: 17.0.2_react@17.0.2 dev: false - /react-with-direction/1.4.0_prpqlkd37azqwypxturxi7uyci: - resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} - peerDependencies: - react: ^0.14 || ^15 || ^16 - react-dom: ^0.14 || ^15 || ^16 - dependencies: - airbnb-prop-types: 2.16.0_react@17.0.2 - brcast: 2.0.2 - deepmerge: 1.5.2 - direction: 1.0.4 - hoist-non-react-statics: 3.3.2 - object.assign: 4.1.4 - object.values: 1.1.5 - prop-types: 15.8.1 - react: 17.0.2 - react-dom: 16.14.0_react@17.0.2 - dev: false - /react-with-direction/1.4.0_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} peerDependencies: @@ -36325,7 +36286,7 @@ packages: dependencies: array.prototype.flat: 1.2.5 global-cache: 1.2.1 - react-with-styles: 3.2.3_wunono5fri6mu4ojuug6cyhj7m + react-with-styles: 3.2.3_f6ta4w4ch5ogxra4gx65xzrqki dev: false /react-with-styles-interface-css/6.0.0_sjrqpgd5uboanyy2xkv2xcu6vm: @@ -36362,7 +36323,7 @@ packages: object.assign: 4.1.4 prop-types: 15.8.1 react: 17.0.2 - react-with-direction: 1.4.0_prpqlkd37azqwypxturxi7uyci + react-with-direction: 1.4.0_sfoxds7t5ydpegc3knd667wn6m dev: false /react-with-styles/3.2.3_wunono5fri6mu4ojuug6cyhj7m: @@ -36596,7 +36557,6 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 reakit-utils: 0.15.2_sfoxds7t5ydpegc3knd667wn6m - dev: false /reakit-system/0.15.2_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-TvRthEz0DmD0rcJkGamMYx+bATwnGNWJpe/lc8UV2Js8nnPvkaxrHk5fX9cVASFrWbaIyegZHCWUBfxr30bmmA==} @@ -36647,7 +36607,6 @@ packages: dependencies: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /reakit-utils/0.15.2_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-i/RYkq+W6hvfFmXw5QW7zvfJJT/K8a4qZ0hjA79T61JAFPGt23DsfxwyBbyK91GZrJ9HMrXFVXWMovsKBc1qEQ==} @@ -36701,7 +36660,6 @@ packages: reakit-utils: 0.15.2_sfoxds7t5ydpegc3knd667wn6m transitivePeerDependencies: - react-dom - dev: false /reakit-warning/0.6.2_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-z/3fvuc46DJyD3nJAUOto6inz2EbSQTjvI/KBQDqxwB0y02HDyeP8IWOJxvkuAUGkWpeSx+H3QWQFSNiPcHtmw==} @@ -36757,7 +36715,6 @@ packages: reakit-system: 0.15.2_sfoxds7t5ydpegc3knd667wn6m reakit-utils: 0.15.2_sfoxds7t5ydpegc3knd667wn6m reakit-warning: 0.6.2_sfoxds7t5ydpegc3knd667wn6m - dev: false /reakit/1.3.11_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-mYxw2z0fsJNOQKAEn5FJCPTU3rcrY33YZ/HzoWqZX0G7FwySp1wkCYW79WhuYMNIUFQ8s3Baob1RtsEywmZSig==} @@ -36807,7 +36764,6 @@ packages: engines: {node: '>= 0.10'} dependencies: resolve: 1.22.1 - dev: true /redent/3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} @@ -37083,7 +37039,6 @@ packages: /rememo/4.0.0: resolution: {integrity: sha512-6BAfg1Dqg6UteZBEH9k6EHHersM86/EcBOMtJV+h+xEn1GC3H+gAgJWpexWYAamAxD0qXNmIt50iS/zuZKnQag==} - dev: false /remove-accents/0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} @@ -37190,7 +37145,6 @@ packages: /requestidlecallback/0.3.0: resolution: {integrity: sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==} - dev: false /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -37505,7 +37459,7 @@ packages: sass: 1.49.9 schema-utils: 3.1.1 semver: 7.3.5 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 dev: true /sass-loader/10.2.1_webpack@5.70.0: @@ -37889,7 +37843,6 @@ packages: hasBin: true dependencies: yargs: 14.2.3 - dev: false /shx/0.3.4: resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} @@ -37921,7 +37874,6 @@ packages: /simple-html-tokenizer/0.5.11: resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} - dev: false /simple-swizzle/0.2.2: resolution: {integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=} @@ -38668,7 +38620,6 @@ packages: dependencies: hey-listen: 1.0.8 tslib: 2.3.1 - dev: false /styled-griddie/0.1.3: resolution: {integrity: sha512-RjsiiADJrRpdPTF8NR26nlZutnvkrX78tiM5/za/E+ftVdpjD8ZBb2iOzrIzfix80uDcHYQbg3iIR0lOGaYmEQ==} @@ -38702,7 +38653,7 @@ packages: dependencies: stylelint: 13.13.1 stylelint-config-recommended: 5.0.0_stylelint@13.13.1 - stylelint-scss: 3.21.0_stylelint@13.13.1 + stylelint-scss: 3.21.0_stylelint@14.6.0 dev: true /stylelint-config-recommended-scss/4.3.0_osgfwlh245rsrcikctalltinom: @@ -38779,7 +38730,7 @@ packages: stylelint: 13.13.1 stylelint-config-recommended: 3.0.0_stylelint@13.13.1 stylelint-config-recommended-scss: 4.3.0_2vkgt733dnumio3be4grtjqkwy - stylelint-scss: 3.21.0_stylelint@13.13.1 + stylelint-scss: 3.21.0_stylelint@14.6.0 dev: true /stylelint-scss/3.21.0_stylelint@13.13.1: @@ -38810,6 +38761,20 @@ packages: stylelint: 13.8.0 dev: true + /stylelint-scss/3.21.0_stylelint@14.6.0: + resolution: {integrity: sha512-CMI2wSHL+XVlNExpauy/+DbUcB/oUZLARDtMIXkpV/5yd8nthzylYd1cdHeDMJVBXeYHldsnebUX6MoV5zPW4A==} + engines: {node: '>=8'} + peerDependencies: + stylelint: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 + dependencies: + lodash: 4.17.21 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.1 + postcss-selector-parser: 6.0.9 + postcss-value-parser: 4.2.0 + stylelint: 14.6.0 + dev: true + /stylelint-scss/4.2.0_stylelint@14.6.0: resolution: {integrity: sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==} peerDependencies: @@ -38863,7 +38828,7 @@ packages: postcss-sass: 0.4.4 postcss-scss: 2.1.1 postcss-selector-parser: 6.0.6 - postcss-syntax: 0.36.2_postcss@7.0.39 + postcss-syntax: 0.36.2_postcss@8.4.12 postcss-value-parser: 4.1.0 resolve-from: 5.0.0 slash: 3.0.0 @@ -38993,7 +38958,6 @@ packages: /stylis/4.0.13: resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==} - dev: false /sudo-prompt/9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} @@ -39438,7 +39402,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.8.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 transitivePeerDependencies: - acorn @@ -39762,7 +39726,6 @@ packages: /traverse/0.6.6: resolution: {integrity: sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw==} - dev: false /tree-kill/1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} @@ -39803,7 +39766,6 @@ packages: /tryer/1.0.1: resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} - dev: true /ts-dedent/2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -40010,7 +39972,6 @@ packages: /tslib/2.0.1: resolution: {integrity: sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==} - dev: false /tslib/2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -40632,7 +40593,7 @@ packages: loader-utils: 1.4.0 mime: 2.5.2 schema-utils: 1.0.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 dev: true /url-loader/3.0.0_webpack@4.46.0: @@ -40831,7 +40792,6 @@ packages: date-fns: 2.29.3 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /use-memo-one/1.1.2_react@16.14.0: resolution: {integrity: sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==} @@ -41273,7 +41233,6 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true /webpack-bundle-analyzer/4.6.1: resolution: {integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==} @@ -41369,7 +41328,6 @@ packages: webpack: 5.70.0_webpack-cli@4.9.2 webpack-bundle-analyzer: 3.9.0 webpack-merge: 5.8.0 - dev: true /webpack-cli/4.9.2_webpack@5.70.0: resolution: {integrity: sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==} @@ -41496,7 +41454,6 @@ packages: dependencies: clone-deep: 4.0.1 wildcard: 2.0.0 - dev: true /webpack-remove-empty-scripts/0.7.3_webpack@5.70.0: resolution: {integrity: sha512-yipqb25A0qtH7X9vKt6yihwyYkTtSlRiDdBb2QsyrkqGM3hpfAcfOO1lYDef9HQUNm3s8ojmorbNg32XXX6FYg==} @@ -41621,7 +41578,7 @@ packages: tapable: 1.1.3 terser-webpack-plugin: 1.4.5_webpack@4.46.0 watchpack: 1.7.5 - webpack-cli: 3.3.12_webpack@5.70.0 + webpack-cli: 3.3.12_webpack@4.46.0 webpack-sources: 1.4.3 transitivePeerDependencies: - supports-color @@ -41786,7 +41743,6 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: true /websocket-driver/0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} @@ -41926,7 +41882,6 @@ packages: /wildcard/2.0.0: resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} - dev: true /window-size/0.2.0: resolution: {integrity: sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=} @@ -42242,7 +42197,6 @@ packages: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - dev: false /yargs-parser/18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} @@ -42304,7 +42258,6 @@ packages: which-module: 2.0.0 y18n: 4.0.3 yargs-parser: 15.0.3 - dev: false /yargs/15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} @@ -42511,4 +42464,4 @@ packages: - react-native - supports-color - utf-8-validate - dev: false \ No newline at end of file + dev: false From 4775defb1fc0e8889a31f7bd822783f4061004d0 Mon Sep 17 00:00:00 2001 From: Moon Date: Tue, 18 Oct 2022 09:22:51 -0700 Subject: [PATCH 013/149] Update WooCommerce beta tester README (#35090) * Update readme * Add changelog * Add Installation section * Fix typo * Update WC Admin Test Helper dev commands * Move back Development section to README.md --- .../EXTENDING-WC-ADMIN-HELPER.md | 91 ++++++++++++ plugins/woocommerce-beta-tester/README.md | 129 ++---------------- .../update-woocommerce-beta-tester-readme | 4 + 3 files changed, 104 insertions(+), 120 deletions(-) create mode 100644 plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md create mode 100644 plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme diff --git a/plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md b/plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md new file mode 100644 index 00000000000..0060ed451e8 --- /dev/null +++ b/plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md @@ -0,0 +1,91 @@ +## Extending + +There are two client-side filters available if you want to extend the test +helper with your own plugin's test setup code. + +This example adds a new tab: + +``` +import { addFilter } from '@wordpress/hooks'; + +const SuperSekret = () => ( + <> +

Super sekret

+

This section contains super sekret tools.

+ + +); +addFilter( + 'woocommerce_admin_test_helper_tabs', + 'wath', + ( tabs ) => [ + ...tabs, + { + name: 'super-sekret', + title: 'Super sekret', + content: , + } + ] +); +``` + +This example adds a new tool to the existing Options tab: + +``` +import { addFilter } from '@wordpress/hooks'; + +const NewTool = () => ( + <> + New tool +

Description

+ + +); +addFilter( + 'woocommerce_admin_test_helper_tab_options', + 'wath', + ( entries ) => [ + ...entries, + + ] +); +``` + +Register a REST API endpoint to perform server-side actions in the usual way: + +``` +add_action( 'rest_api_init', function() { + register_rest_route( + 'your-plugin/v1', + '/area/action', + array( + 'methods' => 'POST', + 'callback' => 'your_plugin_area_action', + 'permission_callback' => function( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit ) ) { + return new \WP_Error( + 'woocommerce_rest_cannot_edit', + __( 'Sorry, you cannot perform this action', 'your-plugin' ) + ); + } + return true; + } + ) + ); +} ); + +function your_plugin_area_action() { + return []; +} +``` + +This would be used on the client like this: + +``` +import apiFetch from '@wordpress/api-fetch'; +... +const response = await apiFetch( { + path: '/your-plugin/v1/area/action', + method: 'POST', +} ); +``` diff --git a/plugins/woocommerce-beta-tester/README.md b/plugins/woocommerce-beta-tester/README.md index 3b36bd7fd56..65a1889ebfb 100644 --- a/plugins/woocommerce-beta-tester/README.md +++ b/plugins/woocommerce-beta-tester/README.md @@ -1,137 +1,26 @@ # WooCommerce Beta Tester -A plugin that makes it easy to test out pre-releases such as betas release canadidates and even final releases. +A plugin that makes it easy to test out pre-releases such as betas release candidates and even final releases. It also comes with WooCommerce Admin Test Helper that helps test WooCommerce Admin functionalities. -## Usage +## Installation -You can get to the settings and features from your top admin bar under the name WC Beta Tester. - -# WooCommerce Admin Test Helper - -A plugin that makes it easier to test the WooCommerce Admin plugin. +You can either install the latest version from [wp.org](https://wordpress.org/plugins/woocommerce-beta-tester/) or symlink this directory by running `ln -s ./ :path-to-your-wp-plugin-directory/woocommerce-beta-tester` ## Development To get started, run the following commands: ```text -npm install -npm start +pnpm install +pnpm run start ``` See [wp-scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts) for more usage information. -## Extending +## Usage -There are two client-side filters available if you want to extend the test -helper with your own plugin's test setup code. +You can get to the settings and features from your top admin bar under the name WC Beta Tester. -This example adds a new tab: +For more information about WooCommerce Admin Test Helper usage, click [here](./EXTENDING-WC-ADMIN-HELPER.md). -``` -import { addFilter } from '@wordpress/hooks'; - -const SuperSekret = () => ( - <> -

Super sekret

-

This section contains super sekret tools.

- - -); -addFilter( - 'woocommerce_admin_test_helper_tabs', - 'wath', - ( tabs ) => [ - ...tabs, - { - name: 'super-sekret', - title: 'Super sekret', - content: , - } - ] -); -``` - -This example adds a new tool to the existing Options tab: - -``` -import { addFilter } from '@wordpress/hooks'; - -const NewTool = () => ( - <> - New tool -

Description

- - -); -addFilter( - 'woocommerce_admin_test_helper_tab_options', - 'wath', - ( entries ) => [ - ...entries, - - ] -); -``` - -Register a REST API endpoint to perform server-side actions in the usual way: - -``` -add_action( 'rest_api_init', function() { - register_rest_route( - 'your-plugin/v1', - '/area/action', - array( - 'methods' => 'POST', - 'callback' => 'your_plugin_area_action', - 'permission_callback' => function( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'edit ) ) { - return new \WP_Error( - 'woocommerce_rest_cannot_edit', - __( 'Sorry, you cannot perform this action', 'your-plugin' ) - ); - } - return true; - } - ) - ); -} ); - -function your_plugin_area_action() { - return []; -} -``` - -This would be used on the client like this: - -``` -import apiFetch from '@wordpress/api-fetch'; -... -const response = await apiFetch( { - path: '/your-plugin/v1/area/action', - method: 'POST', -} ); -``` - -### Deploying - -Prerequisites: - -- [Hub](https://github.com/github/hub) -- Write access to this repository - -You can create a test ZIP of the plugin using this command: - -``` -npm run build -``` - -This creates `woocommerce-admin-test-helper.zip` in the project root. - -We release the plugin using GitHub Releases. There is a script to automate this: - -0. Make sure the version is updated in `woocommerce-admin-test-helper.php` and `package.json` -1. Commit and push to `trunk` -2. Run `npm run release` -3. Make sure you provide the correct version number when prompted -4. That's it! +Run `./bin/build-zip.sh` to make a zip file. diff --git a/plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme b/plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme new file mode 100644 index 00000000000..ce36805e219 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update README to separate WooCommerce Admin Tester From a0b27a4966486472ba68506b45c2b22a5f717544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 18 Oct 2022 15:40:56 -0300 Subject: [PATCH 014/149] Select the current new added shipping class (#35123) * Select the current new added shipping class * Extracting constants into the right file * Add unit tests * Use setValue instead onChange to select the shipping class of the product --- .../client/products/constants.js | 3 - .../client/products/constants.ts | 7 ++ .../sections/product-shipping-section.tsx | 14 ++-- .../test/product-shipping-section.spec.tsx | 79 +++++++++++++++++++ .../changelog/add-35036-select-created-class | 4 + 5 files changed, 97 insertions(+), 10 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/products/constants.js create mode 100644 plugins/woocommerce-admin/client/products/constants.ts create mode 100644 plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx create mode 100644 plugins/woocommerce/changelog/add-35036-select-created-class diff --git a/plugins/woocommerce-admin/client/products/constants.js b/plugins/woocommerce-admin/client/products/constants.js deleted file mode 100644 index 2942144a2a7..00000000000 --- a/plugins/woocommerce-admin/client/products/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const NUMBERS_AND_ALLOWED_CHARS = '[^-0-9%s1%s2]'; -export const NUMBERS_AND_DECIMAL_SEPARATOR = '[^-\\d\\%s]+'; -export const ONLY_ONE_DECIMAL_SEPARATOR = '[%s](?=%s*[%s])'; diff --git a/plugins/woocommerce-admin/client/products/constants.ts b/plugins/woocommerce-admin/client/products/constants.ts new file mode 100644 index 00000000000..5a340309aea --- /dev/null +++ b/plugins/woocommerce-admin/client/products/constants.ts @@ -0,0 +1,7 @@ +export const NUMBERS_AND_ALLOWED_CHARS = '[^-0-9%s1%s2]'; +export const NUMBERS_AND_DECIMAL_SEPARATOR = '[^-\\d\\%s]+'; +export const ONLY_ONE_DECIMAL_SEPARATOR = '[%s](?=%s*[%s])'; +// This should never be a real slug value of any existing shipping class +export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = + '__ADD_NEW_SHIPPING_CLASS_OPTION__'; +export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized'; diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx index 2f57386860c..91fdc9e4bac 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx @@ -34,15 +34,16 @@ import { useProductHelper } from '../use-product-helper'; import { AddNewShippingClassModal } from '../shared/add-new-shipping-class-modal'; import { getTextControlProps } from './utils'; import './product-shipping-section.scss'; +import { + ADD_NEW_SHIPPING_CLASS_OPTION_VALUE, + UNCATEGORIZED_CATEGORY_SLUG, +} from '../constants'; export type ProductShippingSectionProps = { product?: PartialProduct; }; -// This should never be a real slug value of any existing shipping class -const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = '__ADD_NEW_SHIPPING_CLASS_OPTION__'; - -const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ +export const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ { value: '', label: __( 'No shipping class', 'woocommerce' ) }, { value: ADD_NEW_SHIPPING_CLASS_OPTION_VALUE, @@ -50,8 +51,6 @@ const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ }, ]; -const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized'; - function mapShippingClassToSelectOption( shippingClasses: ProductShippingClass[] ): SelectControl.Option[] { @@ -95,7 +94,7 @@ function extractDefaultShippingClassFromProduct( export function ProductShippingSection( { product, }: ProductShippingSectionProps ) { - const { getInputProps } = useFormContext< PartialProduct >(); + const { getInputProps, setValue } = useFormContext< PartialProduct >(); const { formatNumber, parseNumber } = useProductHelper(); const [ highlightSide, setHighlightSide ] = useState< ShippingDimensionsImageProps[ 'highlight' ] >(); @@ -374,6 +373,7 @@ export function ProductShippingSection( { Promise< ProductShippingClass > >( values ).then( ( value ) => { invalidateResolution( 'getProductShippingClasses' ); + setValue( 'shipping_class', value.slug ); return value; } ) } diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx new file mode 100644 index 00000000000..23f2ac8b4ef --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx @@ -0,0 +1,79 @@ +/** + * External dependencies + */ +import { act, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { Form } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { ProductShippingSection } from '../product-shipping-section'; +import { validate } from '../../product-validation'; +import { ADD_NEW_SHIPPING_CLASS_OPTION_VALUE } from '~/products/constants'; + +jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + useSelect: jest.fn(), + useDispatch: jest.fn(), +} ) ); + +describe( 'ProductShippingSection', () => { + const useSelectMock = useSelect as jest.Mock; + const useDispatchMock = useDispatch as jest.Mock; + + beforeEach( () => { + jest.clearAllMocks(); + } ); + + describe( 'when creating a product', () => { + describe( 'when creating a shipping class', () => { + const newShippingClass = { + name: 'New shipping class', + slug: 'new-shipping-class', + }; + const createProductShippingClass = jest + .fn() + .mockReturnValue( Promise.resolve( newShippingClass ) ); + const invalidateResolution = jest.fn(); + + beforeEach( () => { + useSelectMock.mockReturnValue( { + shippingClasses: [ newShippingClass ], + hasResolvedShippingClasses: true, + } ); + + useDispatchMock.mockReturnValue( { + createProductShippingClass, + invalidateResolution, + } ); + + render( +
+ + + ); + } ); + + it( 'should be selected as the current option', async () => { + const select = screen.getByLabelText( 'Shipping class' ); + act( () => + userEvent.selectOptions( + select, + ADD_NEW_SHIPPING_CLASS_OPTION_VALUE + ) + ); + + const dialog = screen.getByRole( 'dialog' ); + const addButton = within( dialog ).getByText( 'Add' ); + await act( async () => userEvent.click( addButton ) ); + + expect( select ).toHaveDisplayValue( [ + newShippingClass.name, + ] ); + } ); + } ); + } ); +} ); diff --git a/plugins/woocommerce/changelog/add-35036-select-created-class b/plugins/woocommerce/changelog/add-35036-select-created-class new file mode 100644 index 00000000000..bc8c95890d3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35036-select-created-class @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Select the current new added shipping class From c79af1acc5963a2debda74b21ea6ae6ad78b00b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 18 Oct 2022 16:05:01 -0300 Subject: [PATCH 015/149] Replace the trash can icon in the attribute list (#35133) --- .../products/fields/attribute-field/attribute-field.tsx | 4 ++-- .../changelog/enhancement-35092-replace-trash-icon | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35092-replace-trash-icon diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx index 314c33ca411..67772f3ba66 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx @@ -6,7 +6,7 @@ import { Button } from '@wordpress/components'; import { ProductAttribute } from '@woocommerce/data'; import { Text } from '@woocommerce/experimental'; import { Sortable, ListItem } from '@woocommerce/components'; -import { trash } from '@wordpress/icons'; +import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies @@ -112,7 +112,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { { __( 'edit', 'woocommerce' ) }
); + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ }; + +export const MenuSlot: React.FC = () => + createPortal( +
+ +
, + document.body + ); diff --git a/packages/js/components/src/experimental-select-control/stories/index.tsx b/packages/js/components/src/experimental-select-control/stories/index.tsx index c36cae46cfd..c13f9c88ebd 100644 --- a/packages/js/components/src/experimental-select-control/stories/index.tsx +++ b/packages/js/components/src/experimental-select-control/stories/index.tsx @@ -1,7 +1,13 @@ /** * External dependencies */ -import { CheckboxControl, Spinner } from '@wordpress/components'; +import { + Button, + CheckboxControl, + Modal, + SlotFillProvider, + Spinner, +} from '@wordpress/components'; import React from 'react'; import { createElement, useState } from '@wordpress/element'; @@ -11,7 +17,7 @@ import { createElement, useState } from '@wordpress/element'; import { SelectedType, DefaultItemType, getItemLabelType } from '../types'; import { MenuItem } from '../menu-item'; import { SelectControl, selectControlStateChangeTypes } from '../'; -import { Menu } from '../menu'; +import { Menu, MenuSlot } from '../menu'; const sampleItems = [ { value: 'apple', label: 'Apple' }, @@ -365,6 +371,45 @@ export const CustomItemType: React.FC = () => { ); }; +export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => { + const [ isOpen, setOpen ] = useState( true ); + const [ selected, setSelected ] = + useState< SelectedType< DefaultItemType > >(); + const [ selectedTwo, setSelectedTwo ] = + useState< SelectedType< DefaultItemType > >(); + + return ( + + Selected: { JSON.stringify( selected ) } + + { isOpen && ( + setOpen( false ) } + > + item && setSelected( item ) } + onRemove={ () => setSelected( null ) } + /> + item && setSelectedTwo( item ) } + onRemove={ () => setSelectedTwo( null ) } + /> + + ) } + + + ); +}; + export default { title: 'WooCommerce Admin/experimental/SelectControl', component: SelectControl, diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 19663facc15..a4c2248355f 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -49,7 +49,10 @@ export { MenuItem as __experimentalSelectControlMenuItem, MenuItemProps as __experimentalSelectControlMenuItemProps, } from './experimental-select-control/menu-item'; -export { Menu as __experimentalSelectControlMenu } from './experimental-select-control/menu'; +export { + Menu as __experimentalSelectControlMenu, + MenuSlot as __experimentalSelectControlMenuSlot, +} from './experimental-select-control/menu'; export { default as ScrollTo } from './scroll-to'; export { Sortable } from './sortable'; export { ListItem } from './list-item'; From bb42e7892a524364fdf730bd78ee5fd28302f59e Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:33:11 +0100 Subject: [PATCH 018/149] api-core-tests add product reviews crud tests (#35163) --- ...-api-core-tests-product-reviews-crud-tests | 4 + .../tests/products/products-crud.test.js | 267 +++++++++++++++++- 2 files changed, 256 insertions(+), 15 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests diff --git a/plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests new file mode 100644 index 00000000000..ee5edb1ab30 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for product reviews crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js index 017fdc10285..efdf05f6fd4 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js @@ -125,7 +125,7 @@ test.describe('Products API tests: CRUD', () => { // call API to update a product attribute term const response = await request.put(`wp-json/wc/v3/products/attributes/${productAttributeId}/terms/${productAttributeTermId}`, { data: { - name:'Square' + name: 'Square' } }); const responseJSON = await response.json(); @@ -398,10 +398,10 @@ test.describe('Products API tests: CRUD', () => { expect(responseJSON.image).toEqual(null); expect(responseJSON.menu_order).toEqual(0); expect(responseJSON.count).toEqual(0); - + }); - + test('can retrieve all product categories', async ({ request }) => { @@ -485,13 +485,12 @@ test.describe('Products API tests: CRUD', () => { `wp-json/wc/v3/products/categories/batch`, { data: { create: [{ - name: "" + name: "Another Category Name" }, ], update: [{ - id: category1Id, - description: "Put them on your head." - } - ], + id: category1Id, + description: "Put them on your head." + }], delete: [ category2Id ] @@ -523,6 +522,245 @@ test.describe('Products API tests: CRUD', () => { }); }); + test.describe('Product review tests: CRUD', () => { + let productReviewId; + + test('can add a product review', async ({ + request + }) => { + const response = await request.post('wp-json/wc/v3/products/reviews', { + data: { + product_id: productId, + review: "Nice simple product!", + reviewer: "John Doe", + reviewer_email: "john.doe@example.com", + rating: 5 + }, + }); + const responseJSON = await response.json(); + productReviewId = responseJSON.id; + + expect(response.status()).toEqual(201); + expect(typeof productReviewId).toEqual('number'); + expect(responseJSON.id).toEqual(productReviewId); + expect(responseJSON.product_name).toEqual('A Simple Product'); + expect(responseJSON.status).toEqual("approved"); + expect(responseJSON.reviewer).toEqual('John Doe'); + expect(responseJSON.reviewer_email).toEqual('john.doe@example.com'); + expect(responseJSON.review).toEqual("Nice simple product!"); + expect(responseJSON.rating).toEqual(5); + expect(responseJSON.verified).toEqual(false); + }); + + test('cannot add a product review with invalid product_id', async ({ + request + }) => { + const response = await request.post('wp-json/wc/v3/products/reviews', { + data: { + product_id: 999, + review: "A non existant product!", + reviewer: "John Do Not", + reviewer_email: "john.do.not@example.com", + rating: 5 + }, + }); + const responseJSON = await response.json(); + + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual("woocommerce_rest_product_invalid_id"); + expect(responseJSON.message).toEqual("Invalid product ID."); + }); + + test('cannot add a duplicate product review', async ({ + request + }) => { + const response = await request.post('wp-json/wc/v3/products/reviews', { + data: { + product_id: productId, + review: "Nice simple product!", + reviewer: "John Doe", + reviewer_email: "john.doe@example.com", + rating: 5 + }, + }); + const responseJSON = await response.json(); + + expect(response.status()).toEqual(409); + expect(responseJSON.code).toEqual("woocommerce_rest_comment_duplicate"); + expect(responseJSON.message).toEqual("Duplicate comment detected; it looks as though you’ve already said that!"); + }); + + test('can retrieve a product review', async ({ + request + }) => { + const response = await request.get(`wp-json/wc/v3/products/reviews/${productReviewId}`); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.id).toEqual(productReviewId); + expect(responseJSON.product_id).toEqual(productId); + expect(responseJSON.product_name).toEqual('A Simple Product'); + expect(responseJSON.status).toEqual("approved"); + expect(responseJSON.reviewer).toEqual('John Doe'); + expect(responseJSON.reviewer_email).toEqual('john.doe@example.com'); + expect(responseJSON.review).toEqual("

Nice simple product!

\n"); + expect(responseJSON.rating).toEqual(5); + expect(responseJSON.verified).toEqual(false); + + }); + + test('can retrieve all product reviews', async ({ + request + }) => { + // call API to retrieve all product tags + const response = await request.get('/wp-json/wc/v3/products/reviews'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + }); + + test('can update a product review', async ({ + request + }) => { + // call API to retrieve all product tags + const response = await request.put(`wp-json/wc/v3/products/reviews/${productReviewId}`, { + data: { + rating: 1 + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.id).toEqual(productReviewId); + expect(responseJSON.product_id).toEqual(productId); + expect(responseJSON.product_name).toEqual('A Simple Product'); + expect(responseJSON.status).toEqual("approved"); + expect(responseJSON.reviewer).toEqual('John Doe'); + expect(responseJSON.reviewer_email).toEqual('john.doe@example.com'); + expect(responseJSON.review).toEqual("Nice simple product!"); + expect(responseJSON.rating).toEqual(1); + expect(responseJSON.verified).toEqual(false); + }); + + test('can permanently delete a product review', async ({ + request + }) => { + // Delete the product category. + const response = await request.delete( + `wp-json/wc/v3/products/reviews/${productReviewId}`, { + data: { + force: true, + }, + } + ); + expect(response.status()).toEqual(200); + + // Verify that the product review can no longer be retrieved. + const getDeletedProductReviewResponse = await request.get( + `wp-json/wc/v3/products/reviews/${productReviewId}` + ); + /** + * currently returns a 403 (forbidden) rather than a 404 (not found) + * an issue has been raised to track this + * See: https://github.com/woocommerce/woocommerce/issues/35162 + */ + expect(getDeletedProductReviewResponse.status()).toEqual(403); + }); + + test('can batch update product reviews', async ({ + request + }) => { + // Batch create product reviews. + const response = await request.post( + `wp-json/wc/v3/products/reviews/batch`, { + data: { + create: [{ + product_id: productId, + review: "Nice product!", + reviewer: "John Doe", + reviewer_email: "john.doe@example.com", + rating: 4 + }, + { + product_id: productId, + review: "I love this thing!", + reviewer: "Jane Doe", + reviewer_email: "Jane.doe@example.com", + rating: 5 + } + ] + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.create[0].product_id).toEqual(productId); + expect(responseJSON.create[0].review).toEqual('Nice product!'); + expect(responseJSON.create[0].reviewer).toEqual('John Doe'); + expect(responseJSON.create[0].reviewer_email).toEqual('john.doe@example.com'); + expect(responseJSON.create[0].rating).toEqual(4); + + expect(responseJSON.create[1].product_id).toEqual(productId); + expect(responseJSON.create[1].review).toEqual('I love this thing!'); + expect(responseJSON.create[1].reviewer).toEqual('Jane Doe'); + expect(responseJSON.create[1].reviewer_email).toEqual('Jane.doe@example.com'); + expect(responseJSON.create[1].rating).toEqual(5); + const review1Id = responseJSON.create[0].id; + const review2Id = responseJSON.create[1].id; + + // Batch create a new review, update a review and delete another. + const responseBatchUpdate = await request.post( + `wp-json/wc/v3/products/reviews/batch`, { + data: { + create: [{ + product_id: productId, + review: "Ok product.", + reviewer: "Jack Doe", + reviewer_email: "jack.doe@example.com", + rating: 3 + }, ], + update: [{ + id: review1Id, + review: "On reflection, I hate this thing!", + rating: 1 + }], + delete: [ + review2Id + ] + } + } + ); + const responseBatchUpdateJSON = await responseBatchUpdate.json(); + const review3Id = responseBatchUpdateJSON.create[0].id; + expect(response.status()).toEqual(200); + + const responseUpdatedReview = await request.get(`wp-json/wc/v3/products/reviews/${review1Id}`); + const responseUpdatedReviewJSON = await responseUpdatedReview.json(); + expect(responseUpdatedReviewJSON.review).toEqual('

On reflection, I hate this thing!

\n'); + expect(responseUpdatedReviewJSON.rating).toEqual(1); + + + // Verify that the deleted review can no longer be retrieved. + const getDeletedProductReviewResponse = await request.get( + `wp-json/wc/v3/products/reviews/${review2Id}` + ); + /** + * currently returns a 403 (forbidden) rather than a 404 (not found) + * an issue has been raised to track this + * See: https://github.com/woocommerce/woocommerce/issues/35162 + */ + expect(getDeletedProductReviewResponse.status()).toEqual(403); + + // Batch delete the created tags + await request.post( + `wp-json/wc/v3/products/reviews/batch`, { + data: { + delete: [review1Id, review3Id] + } + } + ); + }); + }); + test.describe('Product shipping classes tests: CRUD', () => { let productShippingClassId; @@ -556,10 +794,10 @@ test.describe('Products API tests: CRUD', () => { expect(responseJSON.slug).toEqual('priority'); expect(responseJSON.description).toEqual(''); expect(responseJSON.count).toEqual(0); - + }); - + test('can retrieve all product shipping classes', async ({ request }) => { @@ -641,10 +879,9 @@ test.describe('Products API tests: CRUD', () => { name: "Express" }, ], update: [{ - id: shippingClass1Id, - description: "Priority shipping." - } - ], + id: shippingClass1Id, + description: "Priority shipping." + }], delete: [ shippingClass2Id ] @@ -676,7 +913,7 @@ test.describe('Products API tests: CRUD', () => { }); }); - + test.describe('Product tags tests: CRUD', () => { let productTagId; From 53cb2ada671d457a4cf78267176d5c302ecc57b6 Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Tue, 18 Oct 2022 15:44:20 -0700 Subject: [PATCH 019/149] Revise logic to update changelog.txt entries (#35086) * Add logic to update the Stable version after a release * Revise post release automation to only update changelog --- .github/workflows/post-release.yml | 102 +++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 31bb7dc23c5..c9454db35ef 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -10,52 +10,112 @@ env: GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com' jobs: - update-changelog-in-trunk: - name: Update changelog in trunk + changelog-version-update: + name: Update changelog and version runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - - name: Get tag name - id: tag - uses: actions/github-script@v6 - with: - script: | - const tag = ${{ toJSON( github.event.release.tag_name ) }} - - console.log( `::set-output name=tag::release/${ tag.substring( 0, 3 ) }` ) - name: Git fetch trunk branch run: git fetch origin trunk - - name: Copy changelog.txt to vm root - run: cp changelog.txt ../../changelog.txt + - name: Copy readme.txt to vm root + run: cp ./plugins/woocommerce/readme.txt ../../readme.txt - name: Switch to trunk branch run: git checkout trunk - - - name: Create a new branch based on trunk - run: git checkout -b update/changelog-from-release-${{ github.event.release.tag_name }} - - name: Copy saved changelog.txt to monorepo - run: cp ../../changelog.txt ./changelog.txt + - name: Create a new branch based on trunk + run: git checkout -b prep/post-release-tasks-${{ github.event.release.tag_name }} + + - name: Check if we need to continue processing + uses: actions/github-script@v6 + id: check + with: + script: | + const fs = require( 'node:fs' ); + const version = ${{ toJSON( github.event.release.tag_name ) }} + + fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { + if ( err ) { + console.error( err ); + } + + const regex = /Stable\stag:\s(\d+\.\d+\.\d+)/; + + const stableVersion = data.match( regex )[1]; + + // If the release version is less than stable version we can bail. + if ( version.localeCompare( stableVersion, undefined, { numeric: true, sensitivity: 'base' } ) == -1 ) { + console.log( 'Release version is less than stable version. No automated action taken. A manual process is required.' ); + console.log( `::set-output name=continue::false` ) + return; + } else { + console.log( `::set-output name=continue::true` ) + } + } ) + + - name: Update changelog.txt entries + uses: actions/github-script@v6 + id: update-entries + if: steps.check.outputs.continue == 'true' + with: + script: | + const fs = require( 'node:fs' ); + const version = ${{ toJSON( github.event.release.tag_name ) }} + + // Read the saved readme.txt file from earlier. + fs.readFile( '../../readme.txt', 'utf-8', function( err, readme ) { + if ( err ) { + console.log( `::set-output name=continue::false` ) + console.error( err ); + } + + const regex = /(== Changelog ==[\s\S]+)\s{2}\[See changelog for all versions\]\(https:\/\/raw\.githubusercontent\.com\/woocommerce\/woocommerce\/trunk\/changelog\.txt\)\./; + + const entries = readme.match( regex )[1]; + + fs.readFile( './changelog.txt', 'utf-8', function( err, changelog ) { + if ( err ) { + console.log( `::set-output name=continue::false` ) + console.error( err ); + } + + const regex = /== Changelog ==/; + + const updatedChangelog = changelog.replace( regex, entries ); + + fs.writeFile( './changelog.txt', updatedChangelog, err => { + if ( err ) { + console.log( `::set-output name=continue::false` ) + console.error( 'Unable to update changelog entries in changelog.txt' ); + } + + console.log( `::set-output name=continue::true` ) + } ) + } ) + } ) - name: Commit changes - run: git commit -am "Update changelog.txt from release ${{ github.event.release.tag_name }}" + if: steps.update-entries.outputs.continue == 'true' + run: git commit -am "Prep trunk post release ${{ github.event.release.tag_name }}" - name: Push branch up - run: git push origin update/changelog-from-release-${{ github.event.release.tag_name }} + if: steps.update-entries.outputs.continue == 'true' + run: git push origin prep/post-release-tasks-${{ github.event.release.tag_name }} - name: Create the PR + if: steps.update-entries.outputs.continue == 'true' uses: actions/github-script@v6 with: script: | - const body = "This PR updates the changelog.txt based on the latest release: ${{ github.event.release.tag_name }}" + const body = "This PR updates the changelog.txt entries based on the latest release: ${{ github.event.release.tag_name }}" const pr = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, title: "Update changelog.txt from release ${{ github.event.release.tag_name }}", - head: "update/changelog-from-release-${{ github.event.release.tag_name }}", + head: "prep/post-release-tasks-${{ github.event.release.tag_name }}", base: "trunk", body: body }) From c72e00d85c1eb78ec50693f4e460d66eafca3f8a Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Tue, 18 Oct 2022 16:08:49 -0700 Subject: [PATCH 020/149] Add Tooltip component and remove EnrichedLabel (#35024) * Add experimental tooltip component * Add tooltip stories * Update EnrichedLabel component * Remove EnrichedLabel * Add changelog entries * Fix up linting issues * Handle PR feedback --- packages/js/components/changelog/add-34997 | 4 + .../components/src/enriched-label/README.md | 23 ----- .../src/enriched-label/enriched-label.tsx | 75 ----------------- .../js/components/src/enriched-label/index.js | 1 - .../src/enriched-label/stories/index.js | 44 ---------- .../src/enriched-label/stories/style.scss | 23 ----- .../components/src/enriched-label/style.scss | 18 ---- packages/js/components/src/index.ts | 2 +- packages/js/components/src/style.scss | 2 +- packages/js/components/src/tooltip/index.ts | 1 + .../components/src/tooltip/stories/index.tsx | 36 ++++++++ packages/js/components/src/tooltip/style.scss | 13 +++ .../js/components/src/tooltip/tooltip.tsx | 83 +++++++++++++++++++ .../client/products/product-page.scss | 11 +-- .../sections/product-details-section.scss | 4 - .../sections/product-details-section.tsx | 58 ++++++++----- plugins/woocommerce/changelog/add-34997 | 4 + 17 files changed, 188 insertions(+), 214 deletions(-) create mode 100644 packages/js/components/changelog/add-34997 delete mode 100644 packages/js/components/src/enriched-label/README.md delete mode 100644 packages/js/components/src/enriched-label/enriched-label.tsx delete mode 100644 packages/js/components/src/enriched-label/index.js delete mode 100644 packages/js/components/src/enriched-label/stories/index.js delete mode 100644 packages/js/components/src/enriched-label/stories/style.scss delete mode 100644 packages/js/components/src/enriched-label/style.scss create mode 100644 packages/js/components/src/tooltip/index.ts create mode 100644 packages/js/components/src/tooltip/stories/index.tsx create mode 100644 packages/js/components/src/tooltip/style.scss create mode 100644 packages/js/components/src/tooltip/tooltip.tsx create mode 100644 plugins/woocommerce/changelog/add-34997 diff --git a/packages/js/components/changelog/add-34997 b/packages/js/components/changelog/add-34997 new file mode 100644 index 00000000000..443a11eca4c --- /dev/null +++ b/packages/js/components/changelog/add-34997 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Remove EnrichedLabel component in favor of Tooltip component diff --git a/packages/js/components/src/enriched-label/README.md b/packages/js/components/src/enriched-label/README.md deleted file mode 100644 index a48c1ef982d..00000000000 --- a/packages/js/components/src/enriched-label/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# EnrichedLabel - -Use `EnrichedLabel` to create a label with a tooltip. - -## Usage - -```jsx - alert( 'Learn More clicked' ) } -/> -``` - -### Props - -| Name | Type | Default | Description | -| --------------------- | -------- | ------- | ----------------------------------------------------------------------- | -| `helpDescription` | String | `null` | Text that will be shown in the tooltip. | -| `label` | String | `null` | Text that will be shown in the label. | -| `moreUrl` | String | `null` | URL that will be added to the link `Learn More`, shown after the label. | -| `tooltipLinkCallback` | Function | `noop` | Callback that will be triggered after clicking the `Learn More` link. | diff --git a/packages/js/components/src/enriched-label/enriched-label.tsx b/packages/js/components/src/enriched-label/enriched-label.tsx deleted file mode 100644 index 93055a774bb..00000000000 --- a/packages/js/components/src/enriched-label/enriched-label.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Button, Popover } from '@wordpress/components'; -import { createElement, Fragment, useState } from '@wordpress/element'; -import interpolateComponents from '@automattic/interpolate-components'; -import { Icon, help } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import Link from '../link'; - -type EnrichedLabelProps = { - helpDescription: string; - label: string; - moreUrl: string; - tooltipLinkCallback: () => void; -}; - -export const EnrichedLabel: React.FC< EnrichedLabelProps > = ( { - helpDescription, - label, - moreUrl, - tooltipLinkCallback, -} ) => { - const [ isPopoverVisible, setIsPopoverVisible ] = useState( false ); - - return ( - <> - { label } - { helpDescription && ( -
setIsPopoverVisible( false ) } - > - - - { isPopoverVisible && ( - - { interpolateComponents( { - mixedString: - helpDescription + - ( moreUrl ? ' {{moreLink/}}' : '' ), - components: { - moreLink: moreUrl ? ( - - { __( - 'Learn more', - 'woocommerce' - ) } - - ) : ( -
- ), - }, - } ) } - - ) } -
- ) } - - ); -}; diff --git a/packages/js/components/src/enriched-label/index.js b/packages/js/components/src/enriched-label/index.js deleted file mode 100644 index 52c22688576..00000000000 --- a/packages/js/components/src/enriched-label/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './enriched-label'; diff --git a/packages/js/components/src/enriched-label/stories/index.js b/packages/js/components/src/enriched-label/stories/index.js deleted file mode 100644 index ae0d300bf95..00000000000 --- a/packages/js/components/src/enriched-label/stories/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * External dependencies - */ -import { CheckboxControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { EnrichedLabel } from '../'; -import './style.scss'; - -export default { - title: 'WooCommerce Admin/components/EnrichedLabel', - component: EnrichedLabel, - argTypes: { - tooltipLinkCallback: { action: 'tooltipLinkCallback' }, - }, -}; - -const Template = ( args ) => ( - { - // eslint-disable-next-line no-alert - window.alert( 'Learn More clicked' ); - } } - { ...args } - /> -); - -export const Basic = Template.bind( {} ); -Basic.decorators = [ - ( story, props ) => { - return ( - {} } - /> - ); - }, -]; diff --git a/packages/js/components/src/enriched-label/stories/style.scss b/packages/js/components/src/enriched-label/stories/style.scss deleted file mode 100644 index 1143aceb998..00000000000 --- a/packages/js/components/src/enriched-label/stories/style.scss +++ /dev/null @@ -1,23 +0,0 @@ -.woocommerce-enriched-label-story__checkbox-control { - .woocommerce-enriched-label__help-wrapper { - .components-popover { - margin: 0; - } - } - .components-base-control__field { - display: flex; - .components-checkbox-control { - &__label { - display: flex; - } - - &__input-container { - align-self: center; - } - - .woocommerce-enriched-label__text { - align-self: center; - } - } - } -} diff --git a/packages/js/components/src/enriched-label/style.scss b/packages/js/components/src/enriched-label/style.scss deleted file mode 100644 index a2592f13d75..00000000000 --- a/packages/js/components/src/enriched-label/style.scss +++ /dev/null @@ -1,18 +0,0 @@ -.woocommerce-enriched-label__text { - align-self: center; -} -.woocommerce-enriched-label__help-wrapper { - .components-button { - padding: 0; - height: 28px; - } - .components-popover { - .components-popover__content { - min-width: 360px; - > div { - padding: $gap $gap-large; - font-size: 16px; - } - } - } -} diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index a4c2248355f..58f2182ad5a 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -74,11 +74,11 @@ export { default as Tag } from './tag'; export { default as TextControl } from './text-control'; export { default as TextControlWithAffixes } from './text-control-with-affixes'; export { default as Timeline } from './timeline'; +export { Tooltip as __experimentalTooltip } from './tooltip'; export { default as ViewMoreList } from './view-more-list'; export { default as WebPreview } from './web-preview'; export { Badge } from './badge'; export { DynamicForm } from './dynamic-form'; -export { EnrichedLabel } from './enriched-label'; export { default as TourKit } from './tour-kit'; export * as TourKitTypes from './tour-kit/types'; export { CollapsibleContent } from './collapsible-content'; diff --git a/packages/js/components/src/style.scss b/packages/js/components/src/style.scss index e10f13b53f6..a3d439bc9e2 100644 --- a/packages/js/components/src/style.scss +++ b/packages/js/components/src/style.scss @@ -46,11 +46,11 @@ @import 'tag/style.scss'; @import 'text-control/style.scss'; @import 'text-control-with-affixes/style.scss'; +@import 'tooltip/style.scss'; @import 'timeline/style.scss'; @import 'view-more-list/style.scss'; @import 'web-preview/style.scss'; @import 'badge/style.scss'; @import 'dynamic-form/style.scss'; -@import 'enriched-label/style.scss'; @import 'tour-kit/style.scss'; @import 'collapsible-content/style.scss'; diff --git a/packages/js/components/src/tooltip/index.ts b/packages/js/components/src/tooltip/index.ts new file mode 100644 index 00000000000..ed8326d5e7c --- /dev/null +++ b/packages/js/components/src/tooltip/index.ts @@ -0,0 +1 @@ +export * from './tooltip'; diff --git a/packages/js/components/src/tooltip/stories/index.tsx b/packages/js/components/src/tooltip/stories/index.tsx new file mode 100644 index 00000000000..fe5e3015565 --- /dev/null +++ b/packages/js/components/src/tooltip/stories/index.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { Icon, warning } from '@wordpress/icons'; +import React from 'react'; + +/** + * Internal dependencies + */ +import { Tooltip } from '../'; + +export const Basic = () => { + return ( + + This is a tooltip! + + } + /> + ); +}; + +export const CustomIcon = () => { + return ( + + + + ); +}; + +export default { + title: 'WooCommerce Admin/experimental/Tooltip', + component: Tooltip, +}; diff --git a/packages/js/components/src/tooltip/style.scss b/packages/js/components/src/tooltip/style.scss new file mode 100644 index 00000000000..cba998715b9 --- /dev/null +++ b/packages/js/components/src/tooltip/style.scss @@ -0,0 +1,13 @@ +.woocommerce-tooltip { + display: inline-flex; + + .woocommerce-tooltip__button { + height: auto; + } + + &__text .components-popover__content { + padding: $gap-smaller; + width: max-content; + } +} + diff --git a/packages/js/components/src/tooltip/tooltip.tsx b/packages/js/components/src/tooltip/tooltip.tsx new file mode 100644 index 00000000000..b6b6a38d336 --- /dev/null +++ b/packages/js/components/src/tooltip/tooltip.tsx @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button, Popover } from '@wordpress/components'; +import { createElement, Fragment, useState } from '@wordpress/element'; +import { FocusEvent, KeyboardEvent } from 'react'; +import { Icon, help } from '@wordpress/icons'; + +type Position = + | 'top left' + | 'top right' + | 'top center' + | 'middle left' + | 'middle right' + | 'middle center' + | 'bottom left' + | 'bottom right' + | 'bottom center'; + +type TooltipProps = { + children?: JSX.Element | string; + position?: Position; + text: JSX.Element | string; +}; + +export const Tooltip: React.FC< TooltipProps > = ( { + children = , + position = 'top center', + text, +} ) => { + const [ isPopoverVisible, setIsPopoverVisible ] = useState( false ); + + return ( + <> +
+ + + { isPopoverVisible && ( + { + if ( + event.relatedTarget?.classList.contains( + 'woocommerce-tooltip__button' + ) + ) { + return; + } + setIsPopoverVisible( false ); + } } + onKeyDown={ ( + event: KeyboardEvent< HTMLDivElement > + ) => { + if ( event.key !== 'Escape' ) { + return; + } + setIsPopoverVisible( false ); + } } + > + { text } + + ) } +
+ + ); +}; diff --git a/plugins/woocommerce-admin/client/products/product-page.scss b/plugins/woocommerce-admin/client/products/product-page.scss index 6c8f9adeb0f..d7125bc6f76 100644 --- a/plugins/woocommerce-admin/client/products/product-page.scss +++ b/plugins/woocommerce-admin/client/products/product-page.scss @@ -17,6 +17,12 @@ margin-right: $gap-smaller; } } + .components-checkbox-control__label { + align-items: center; + } + .woocommerce-tooltip { + margin-left: $gap-smaller; + } .woocommerce-product-form { &__custom-label-input { display: flex; @@ -41,11 +47,6 @@ margin-bottom: 0; } } - .woocommerce-enriched-label__help-wrapper { - .components-popover { - margin-top: 0; - } - } } .woocommerce-edit-product { diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss b/plugins/woocommerce-admin/client/products/sections/product-details-section.scss index f276a13c49b..c6f7b22be65 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.scss @@ -25,10 +25,6 @@ align-self: center; } } - .woocommerce-enriched-label__text { - align-self: center; - margin-right: $gap-smaller; - } } } } diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index 1cfb32776ca..910bc0d921f 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -13,10 +13,12 @@ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { cleanForSlug } from '@wordpress/url'; import { - EnrichedLabel, + Link, useFormContext, __experimentalRichTextEditor as RichTextEditor, + __experimentalTooltip as Tooltip, } from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; import { Product, ProductCategory, @@ -30,10 +32,10 @@ import { BlockInstance, serialize, parse } from '@wordpress/blocks'; * Internal dependencies */ import './product-details-section.scss'; +import { CategoryField } from '../fields/category-field'; +import { EditProductLinkModal } from '../shared/edit-product-link-modal'; import { getCheckboxProps, getTextControlProps } from './utils'; import { ProductSectionLayout } from '../layout/product-section-layout'; -import { EditProductLinkModal } from '../shared/edit-product-link-modal'; -import { CategoryField } from '../fields/category-field'; const PRODUCT_DETAILS_SLUG = 'product-details'; @@ -133,22 +135,40 @@ export const ProductDetailsSection: React.FC = () => { /> - recordEvent( 'add_product_learn_more', { - category: PRODUCT_DETAILS_SLUG, - } ) - } - /> + <> + { __( 'Feature this product', 'woocommerce' ) } + + recordEvent( + 'add_product_learn_more', + { + category: + PRODUCT_DETAILS_SLUG, + } + ) + } + > + { __( + 'Learn more', + 'woocommerce' + ) } + + ), + }, + } ) } + /> + } { ...getCheckboxProps( { ...getInputProps( 'featured' ), diff --git a/plugins/woocommerce/changelog/add-34997 b/plugins/woocommerce/changelog/add-34997 new file mode 100644 index 00000000000..019d0a61d8f --- /dev/null +++ b/plugins/woocommerce/changelog/add-34997 @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Use new Tooltip component instead of EnrichedLabel From a2076f59a57a12cfc62b655d733e5f36b7e9edca Mon Sep 17 00:00:00 2001 From: Jonathan Lane Date: Tue, 18 Oct 2022 23:00:09 -0700 Subject: [PATCH 021/149] Update PR template to provide guidance for testing (#34597) * Update PR template to provide guidance for testing Co-authored-by: Jon Lane --- .github/PULL_REQUEST_TEMPLATE.md | 13 +++++++++++-- ...tch-improve-pr-template-for-testing-instructions | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7f0abf47ce2..a264136cb71 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,18 +14,27 @@ Closes # . + + +- [ ] This PR is a very minor change/addition and does not require testing instructions (if checked you can ignore/remove the next section). + + + ### How to test the changes in this Pull Request: + + 1. 2. 3. + + ### Other information: - [ ] Have you added an explanation of what your changes do and why you'd like us to include them? - [ ] Have you written new tests for your changes, as applicable? -- [ ] Have you successfully run tests with your changes locally? -- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter= run changelog add`? +- [ ] Have you created a changelog file for each project being changed, ie `pnpm changelog add --filter=`? diff --git a/plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions b/plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions new file mode 100644 index 00000000000..435ea696c2e --- /dev/null +++ b/plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Tweaks the PR template for GitHub pull requests From 5b6ddf0b88d720a185ceb3eb7c449f712f27f926 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 19 Oct 2022 16:21:42 +0530 Subject: [PATCH 022/149] Update biling and shipping address indexes. (#35121) * Update biling and shipping address indexes. * Add changelog. * Code standard fixes. * Add unit test for search after update. * PHPCS fixes. --- plugins/woocommerce/changelog/fix-34989 | 4 + .../Orders/OrdersTableDataStore.php | 21 +++- .../Orders/OrdersTableDataStoreTests.php | 99 +++++++++++++------ 3 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-34989 diff --git a/plugins/woocommerce/changelog/fix-34989 b/plugins/woocommerce/changelog/fix-34989 new file mode 100644 index 00000000000..32a14ff79bb --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34989 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add billing and shipping address indexes on order update. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index ac83d682818..a3c11b4f816 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1008,8 +1008,8 @@ WHERE * @return bool Whether the order should be synced. */ private function should_sync_order( \WC_Abstract_Order $order ) : bool { - $draft_order = in_array( $order->get_status(), array( 'draft', 'auto-draft' ) ); - $already_synced = in_array( $order->get_id(), self::$reading_order_ids ); + $draft_order = in_array( $order->get_status(), array( 'draft', 'auto-draft' ), true ); + $already_synced = in_array( $order->get_id(), self::$reading_order_ids, true ); return ! $draft_order && ! $already_synced; } @@ -1017,8 +1017,8 @@ WHERE * Helper method to initialize order object from DB data. * * @param \WC_Abstract_Order $order Order object. - * @param int $order_id Order ID. - * @param \stdClass $order_data Order data fetched from DB. + * @param int $order_id Order ID. + * @param \stdClass $order_data Order data fetched from DB. * * @return void */ @@ -1544,6 +1544,9 @@ FROM $order_meta_table throw new \Exception( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) ); } } + + $changes = $order->get_changes(); + $this->update_address_index_meta( $order, $changes ); } /** @@ -2025,7 +2028,17 @@ FROM $order_meta_table */ public function update_order_meta( &$order ) { $changes = $order->get_changes(); + $this->update_address_index_meta( $order, $changes ); + } + /** + * Helper function to update billing and shipping address metadata. + * @param \WC_Abstract_Order $order Order Object + * @param array $changes Array of changes. + * + * @return void + */ + private function update_address_index_meta( $order, $changes ) { // If address changed, store concatenated version to make searches faster. foreach ( array( 'billing', 'shipping' ) as $address_type ) { if ( isset( $changes[ $address_type ] ) ) { diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index e0b2840e719..15883632971 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -187,10 +187,12 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'status' => 'on-hold', 'cart_hash' => 'YET-ANOTHER-CART-HASH', ); + static $datastore_updates = array( 'email_sent' => true, 'order_stock_reduced' => true, ); + static $meta_to_update = array( 'my_meta_key' => array( 'my', 'custom', 'meta' ), ); @@ -1003,10 +1005,14 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * @testDox Test `get_unpaid_orders()`. */ public function test_get_unpaid_orders(): void { + // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- Intentional usage since timezone is changed for this file. $now = current_time( 'timestamp' ); // Create a few orders. - $orders_by_status = array( 'wc-completed' => 3, 'wc-pending' => 2 ); + $orders_by_status = array( + 'wc-completed' => 3, + 'wc-pending' => 2, + ); $unpaid_ids = array(); foreach ( $orders_by_status as $order_status => $order_count ) { foreach ( range( 1, $order_count ) as $_ ) { @@ -1206,7 +1212,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { /** * Helper method to allow switching data stores. * - * @param WC_Order $order Order object. + * @param WC_Order $order Order object. * @param WC_Data_Store $data_store Data store object to switch order to. */ private function switch_data_store( $order, $data_store ) { @@ -1228,6 +1234,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $order_1 = new WC_Order(); $order_1->set_billing_city( 'Fort Quality' ); $this->switch_data_store( $order_1, $this->sut ); + $this->disable_cot_sync(); $order_1->save(); $product = new WC_Product_Simple(); @@ -1257,14 +1264,43 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { ); // Order 1's billing address references "Quality" and so does one of Order 2's order items. - $query = new OrdersTableQuery( array( 's' => 'Quality' ) ); + $query = new OrdersTableQuery( array( 's' => 'Quality' ) ); + $orders_array = $query->orders; + sort( $orders_array ); $this->assertEquals( array( $order_1->get_id(), $order_2->get_id() ), - $query->orders, + $orders_array, 'Search terms match against address data as well as order item names.' ); } + /** + * @testDox Ensure search works as expected on updated orders. + */ + public function test_cot_query_search_update() { + $order_1 = new WC_Order(); + $this->switch_data_store( $order_1, $this->sut ); + $this->disable_cot_sync(); + $order_1->save(); + + $order_1->set_billing_city( 'New Cybertron' ); + $order_1->save(); + + $order_2 = new WC_Order(); + $this->switch_data_store( $order_2, $this->sut ); + $order_2->save(); + + $order_2->set_billing_city( 'Gigantian City' ); + $order_2->save(); + + $query = new OrdersTableQuery( array( 's' => 'Cybertron' ) ); + $this->assertEquals( + array( $order_1->get_id() ), + $query->orders, + 'Search terms match against updated address data.' + ); + } + /** * Test methods get_total_tax_refunded and get_total_shipping_refunded. */ @@ -1374,7 +1410,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'order_key', 'value' => 'planck_1', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders ); @@ -1408,8 +1444,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'order_key', 'value' => '[0-9]$', - 'compare' => 'RLIKE' - ) + 'compare' => 'RLIKE', + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertEqualsCanonicalizing( $order_ids, $query->orders ); @@ -1419,8 +1455,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'order_key', 'value' => '[^0-9]$', - 'compare' => 'NOT RLIKE' - ) + 'compare' => 'NOT RLIKE', + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1432,7 +1468,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'value' => '10.0', 'compare' => '<=', 'type' => 'NUMERIC', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertEqualsCanonicalizing( array( $order_ids[2] ), $query->orders ); @@ -1442,7 +1478,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'non_existing_field', 'value' => 'any-value', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1453,7 +1489,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'wc_orders.total_amount', 'value' => 5.5, 'compare' => 'IN', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1464,7 +1500,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'wc_orders.total_amount', 'value' => 10.0, 'compare' => 'EXOSTS', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1475,7 +1511,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'total', 'compare' => 'BETWEEN', 'value' => 10.0, - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1486,12 +1522,12 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'total', 'compare' => 'NOT BETWEEN', 'value' => array( 1.0 ), - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); - // Test combinations of field_query with regular query args: + // Test combinations of field_query with regular query args. $args = array( 'id' => array( $order_ids[0], $order_ids[1] ), ); @@ -1506,7 +1542,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'id', 'value' => $order_ids[1], - ) + ), ); $query = new OrdersTableQuery( $args ); $this->assertEqualsCanonicalizing( array( $order_ids[1] ), $query->orders ); @@ -1520,7 +1556,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $query = new OrdersTableQuery( $args ); $this->assertCount( 0, $query->orders ); - // Now a more complex query with meta_query and date_query: + // Now a more complex query with meta_query and date_query. $args = array( 'shipping_address' => 'The Universe', 'field_query' => array( @@ -1537,12 +1573,13 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders ); // ... but only Planck is more than 80 years old. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Intentional usage for test. $args['meta_query'] = array( array( 'key' => 'customer_age', 'value' => 80, - 'compare' => '>=' - ) + 'compare' => '>=', + ), ); $query = new OrdersTableQuery( $args ); $this->assertEqualsCanonicalizing( array( $order_ids[1] ), $query->orders ); @@ -1604,10 +1641,10 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { /** * Helper method to assert props are set. * - * @param array $props List of props to test. + * @param array $props List of props to test. * @param WC_Order $order Order object. - * @param mixed $value Value to assert. - * @param array $ds_getter_setter_names List of props with custom getter/setter names. + * @param mixed $value Value to assert. + * @param array $ds_getter_setter_names List of props with custom getter/setter names. */ private function assert_get_prop_via_ds_object_and_metadata( array $props, WC_Order $order, $value, array $ds_getter_setter_names ) { wp_cache_flush(); @@ -1680,8 +1717,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to set prop via data store. * * @param WC_Order $order Order object. - * @param array $props List of props and their setter names. - * @param mixed $value value to set. + * @param array $props List of props and their setter names. + * @param mixed $value value to set. */ private function set_props_via_data_store( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1693,8 +1730,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to set prop value via object. * * @param WC_Order $order Order object. - * @param array $props List of props and their setter names. - * @param mixed $value value to set. + * @param array $props List of props and their setter names. + * @param mixed $value value to set. */ private function set_props_via_order_object( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1707,8 +1744,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to assert prop value via data store. * * @param WC_Order $order Order object. - * @param array $props List of props and their getter names. - * @param mixed $value value to assert. + * @param array $props List of props and their getter names. + * @param mixed $value value to assert. */ private function assert_props_value_via_data_store( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1720,8 +1757,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to assert prop value via order object. * * @param WC_Order $order Order object. - * @param array $props List of props and their getter names. - * @param mixed $value value to assert. + * @param array $props List of props and their getter names. + * @param mixed $value value to assert. */ private function assert_props_value_via_order_object( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { From 5f2c656e6b4f8ee3749d05c2d0672addc88cf1a5 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Wed, 19 Oct 2022 16:28:29 -0300 Subject: [PATCH 023/149] Add/34331 add attributes modal (#34999) * Add initial add attribute modal * Add async select control component and add attribute terms * Make use of AsyncSelectControl for attributes * Rearranged the add attribute form to make removing easier * Make sure add button is disabled if fields are empty * Remove the use of AsyncSelectControl for now * Add disabled option and fix merge conflict * Add attribute modal tests * Remove unused trigger drag * Add popover slot * Small update to select control and fix multi selection in term field * Add tests for attribute and attribute term fields * Add changelogs * Small fix after merge conflict * Fix some styling and issue with select control when clearing item * Fix lint error * Fix up some styling issues after rebase * Fix formatting, some styling issues, and address some PR feedback * And confirmation dialog for closing the modal. --- .../changelog/add-34331_add_attributes_modal | 4 + .../src/experimental-select-control/menu.tsx | 4 +- .../select-control.tsx | 34 +- .../changelog/add-34331_add_attributes_modal | 4 + packages/js/data/src/crud/resolvers.ts | 2 +- packages/js/data/src/index.ts | 8 + .../data/src/product-attribute-terms/types.ts | 2 +- .../js/data/src/product-attributes/types.ts | 8 +- .../attribute-field/add-attribute-modal.scss | 63 ++++ .../attribute-field/add-attribute-modal.tsx | 328 ++++++++++++++++++ .../attribute-field/attribute-field.scss | 2 +- .../attribute-field/attribute-field.tsx | 102 ++++-- .../test/add-attribute-modal.spec.tsx | 289 +++++++++++++++ .../attribute-input-field.tsx | 115 ++++++ .../fields/attribute-input-field/index.ts | 1 + .../test/attribute-input-field.spec.tsx | 226 ++++++++++++ .../attribute-term-input-field.scss | 5 + .../attribute-term-input-field.tsx | 190 ++++++++++ .../attribute-term-input-field/index.ts | 1 + .../test/attribute-term-input-field.spec.tsx | 208 +++++++++++ .../fields/category-field/category-field.scss | 3 - .../client/products/product-form-actions.scss | 4 +- .../products/sections/attributes-section.tsx | 7 +- .../changelog/add-34331_add_attributes_modal | 4 + 24 files changed, 1553 insertions(+), 61 deletions(-) create mode 100644 packages/js/components/changelog/add-34331_add_attributes_modal create mode 100644 packages/js/data/changelog/add-34331_add_attributes_modal create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx create mode 100644 plugins/woocommerce/changelog/add-34331_add_attributes_modal diff --git a/packages/js/components/changelog/add-34331_add_attributes_modal b/packages/js/components/changelog/add-34331_add_attributes_modal new file mode 100644 index 00000000000..e252934b519 --- /dev/null +++ b/packages/js/components/changelog/add-34331_add_attributes_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add disabled option to the Select Control input component and alter the onInputChange callback diff --git a/packages/js/components/src/experimental-select-control/menu.tsx b/packages/js/components/src/experimental-select-control/menu.tsx index 5b5cd8de526..f4ad3a1878b 100644 --- a/packages/js/components/src/experimental-select-control/menu.tsx +++ b/packages/js/components/src/experimental-select-control/menu.tsx @@ -9,6 +9,7 @@ import { useRef, useState, createPortal, + Children, } from '@wordpress/element'; /** @@ -56,8 +57,7 @@ export const Menu = ( { 'woocommerce-experimental-select-control__popover-menu', { 'is-open': isOpen, - 'has-results': - Array.isArray( children ) && children.length > 0, + 'has-results': Children.count( children ) > 0, } ) } position="bottom center" diff --git a/packages/js/components/src/experimental-select-control/select-control.tsx b/packages/js/components/src/experimental-select-control/select-control.tsx index f6bc95457d5..8545e652112 100644 --- a/packages/js/components/src/experimental-select-control/select-control.tsx +++ b/packages/js/components/src/experimental-select-control/select-control.tsx @@ -48,7 +48,10 @@ type SelectControlProps< ItemType > = { ) => ItemType[]; hasExternalTags?: boolean; multiple?: boolean; - onInputChange?: ( value: string | undefined ) => void; + onInputChange?: ( + value: string | undefined, + changes: Partial< Omit< UseComboboxState< ItemType >, 'inputValue' > > + ) => void; onRemove?: ( item: ItemType ) => void; onSelect?: ( selected: ItemType ) => void; onFocus?: ( data: { inputValue: string } ) => void; @@ -59,6 +62,7 @@ type SelectControlProps< ItemType > = { placeholder?: string; selected: ItemType | ItemType[] | null; className?: string; + disabled?: boolean; }; export const selectControlStateChangeTypes = useCombobox.stateChangeTypes; @@ -102,6 +106,7 @@ function SelectControl< ItemType = DefaultItemType >( { placeholder, selected, className, + disabled, }: SelectControlProps< ItemType > ) { const [ isFocused, setIsFocused ] = useState( false ); const [ inputValue, setInputValue ] = useState( '' ); @@ -150,16 +155,14 @@ function SelectControl< ItemType = DefaultItemType >( { initialSelectedItem: singleSelectedItem, inputValue, items: filteredItems, - selectedItem: multiple ? null : undefined, + selectedItem: multiple ? null : singleSelectedItem, itemToString: getItemLabel, onSelectedItemChange: ( { selectedItem } ) => selectedItem && onSelect( selectedItem ), - onInputValueChange: ( changes ) => { - if ( changes.inputValue !== undefined ) { - setInputValue( changes.inputValue ); - if ( changes.isOpen ) { - onInputChange( changes.inputValue ); - } + onInputValueChange: ( { inputValue: value, ...changes } ) => { + if ( value !== undefined ) { + setInputValue( value ); + onInputChange( value, changes ); } }, stateReducer: ( state, actionAndChanges ) => { @@ -225,12 +228,14 @@ function SelectControl< ItemType = DefaultItemType >( { > { /* Downshift's getLabelProps handles the necessary label attributes. */ } { /* eslint-disable jsx-a11y/label-has-for */ } - + { label && ( + + ) } { /* eslint-enable jsx-a11y/label-has-for */ } ( { }, onBlur: () => setIsFocused( false ), placeholder, + disabled, } ) } > <> diff --git a/packages/js/data/changelog/add-34331_add_attributes_modal b/packages/js/data/changelog/add-34331_add_attributes_modal new file mode 100644 index 00000000000..49cb315e249 --- /dev/null +++ b/packages/js/data/changelog/add-34331_add_attributes_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update product attribute type name and export the product attribute types. diff --git a/packages/js/data/src/crud/resolvers.ts b/packages/js/data/src/crud/resolvers.ts index 6ab06885867..ecfeef5b258 100644 --- a/packages/js/data/src/crud/resolvers.ts +++ b/packages/js/data/src/crud/resolvers.ts @@ -67,7 +67,7 @@ export const createResolvers = ( { } try { - const path = getRestPath( namespace, {}, urlParameters ); + const path = getRestPath( namespace, query || {}, urlParameters ); const { items, totalCount }: { items: Item[]; totalCount: number } = yield request< ItemQuery, Item >( path, resourceQuery ); diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts index 8cde4e16128..be23c55eff3 100644 --- a/packages/js/data/src/index.ts +++ b/packages/js/data/src/index.ts @@ -76,7 +76,15 @@ export * from './countries/types'; export * from './onboarding/types'; export * from './plugins/types'; export * from './products/types'; +export { + QueryProductAttribute, + ProductAttributeSelectors, +} from './product-attributes/types'; export * from './product-shipping-classes/types'; +export { + ProductAttributeTerm, + ProductAttributeTermsSelectors, +} from './product-attribute-terms/types'; export * from './orders/types'; export { ProductCategory, diff --git a/packages/js/data/src/product-attribute-terms/types.ts b/packages/js/data/src/product-attribute-terms/types.ts index eb451eafc8a..6f539fb3df4 100644 --- a/packages/js/data/src/product-attribute-terms/types.ts +++ b/packages/js/data/src/product-attribute-terms/types.ts @@ -8,7 +8,7 @@ import { DispatchFromMap } from '@automattic/data-stores'; */ import { CrudActions, CrudSelectors } from '../crud/types'; -type ProductAttributeTerm = { +export type ProductAttributeTerm = { id: number; slug: string; name: string; diff --git a/packages/js/data/src/product-attributes/types.ts b/packages/js/data/src/product-attributes/types.ts index 11f38ddad18..7222846dead 100644 --- a/packages/js/data/src/product-attributes/types.ts +++ b/packages/js/data/src/product-attributes/types.ts @@ -8,7 +8,7 @@ import { DispatchFromMap } from '@automattic/data-stores'; */ import { CrudActions, CrudSelectors } from '../crud/types'; -type ProductAttribute = { +export type QueryProductAttribute = { id: number; slug: string; name: string; @@ -24,19 +24,19 @@ type Query = { type ReadOnlyProperties = 'id'; type MutableProperties = Partial< - Omit< ProductAttribute, ReadOnlyProperties > + Omit< QueryProductAttribute, ReadOnlyProperties > >; type ProductAttributeActions = CrudActions< 'ProductAttribute', - ProductAttribute, + QueryProductAttribute, MutableProperties >; export type ProductAttributeSelectors = CrudSelectors< 'ProductAttribute', 'ProductAttributes', - ProductAttribute, + QueryProductAttribute, Query, MutableProperties >; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss new file mode 100644 index 00000000000..3453461ef8a --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss @@ -0,0 +1,63 @@ +.woocommerce-add-attribute-modal { + .components-notice.is-info { + margin-left: 0; + margin-right: 0; + background-color: #f0f6fc; + } + + &__add-attribute { + margin-top: $gap-small; + } + + &__buttons { + margin-top: $gap-larger; + display: flex; + flex-direction: row; + gap: 8px; + justify-content: flex-end; + } + + .components-modal__content { + display: flex; + flex-direction: column; + } + + &__body { + min-height: 200px; + flex: 1 1 auto; + overflow: auto; + } + + &__table { + width: 100%; + margin-top: $gap-large; + + th { + text-align: left; + color: $gray-700; + font-weight: normal; + text-transform: uppercase; + } + } + &__table-header { + padding: 0 0 $gap; + } + &__table-header, + &__table-row { + display: grid; + grid-template-columns: 40% 55% 5%; + border-bottom: 1px solid $gray-300; + align-items: center; + } + &__table-row { + padding: $gap-large 0; + td:not(:last-child) { + margin-right: $gap; + } + } + + &__table-attribute-trash-column { + display: flex; + justify-content: center; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx new file mode 100644 index 00000000000..6e2e4e3764d --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx @@ -0,0 +1,328 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { trash } from '@wordpress/icons'; +import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; +import { Form } from '@woocommerce/components'; +import { + Button, + Modal, + Notice, + // @ts-expect-error ConfirmDialog is not part of the typescript definition yet. + __experimentalConfirmDialog as ConfirmDialog, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './add-attribute-modal.scss'; +import { AttributeInputField } from '../attribute-input-field'; +import { AttributeTermInputField } from '../attribute-term-input-field'; + +type CreateCategoryModalProps = { + onCancel: () => void; + onAdd: ( newCategories: ProductAttribute[] ) => void; + selectedAttributeIds?: number[]; +}; + +type AttributeForm = { + attributes: { + attribute?: ProductAttribute; + terms: ProductAttributeTerm[]; + }[]; +}; + +export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { + onCancel, + onAdd, + selectedAttributeIds = [], +} ) => { + const [ showConfirmClose, setShowConfirmClose ] = useState( false ); + const addAnother = ( + values: AttributeForm, + setValue: ( + name: string, + value: AttributeForm[ keyof AttributeForm ] + ) => void + ) => { + setValue( 'attributes', [ + ...values.attributes, + { + attribute: undefined, + terms: [], + }, + ] ); + }; + + const onAddingAttributes = ( values: AttributeForm ) => { + const newAttributesToAdd: ProductAttribute[] = []; + values.attributes.forEach( ( attr ) => { + if ( + attr.attribute && + attr.attribute.name && + attr.terms.length > 0 + ) { + newAttributesToAdd.push( { + ...( attr.attribute as ProductAttribute ), + options: attr.terms.map( ( term ) => term.name ), + } ); + } + } ); + onAdd( newAttributesToAdd ); + }; + + const onRemove = ( + index: number, + values: AttributeForm, + setValue: ( + name: string, + value: AttributeForm[ keyof AttributeForm ] + ) => void + ) => { + if ( values.attributes.length > 1 ) { + setValue( + 'attributes', + values.attributes.filter( ( val, i ) => i !== index ) + ); + } else { + setValue( `attributes[${ index }]`, [ + { attribute: undefined, terms: [] }, + ] ); + } + }; + + const focusValueField = ( index: number ) => { + const valueInputField: HTMLInputElement | null = document.querySelector( + '.woocommerce-add-attribute-modal__table-row-' + + index + + ' .woocommerce-add-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input' + ); + if ( valueInputField ) { + setTimeout( () => { + valueInputField.focus(); + }, 0 ); + } + }; + + const onClose = ( values: AttributeForm ) => { + const hasValuesSet = values.attributes.some( + ( value ) => value?.attribute?.id && value?.terms?.length > 0 + ); + if ( hasValuesSet ) { + setShowConfirmClose( true ); + } else { + onCancel(); + } + }; + + return ( + <> + + initialValues={ { + attributes: [ { attribute: undefined, terms: [] } ], + } } + > + { ( { + values, + setValue, + }: { + values: AttributeForm; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setValue: ( name: string, value: any ) => void; + } ) => { + return ( + onClose( values ) } + className="woocommerce-add-attribute-modal" + > + +

+ { __( + 'By default, attributes are filterable and visible on the product page. You can change these settings for each attribute separately later.', + 'woocommerce' + ) } +

+
+ +
+ + + + + + + + + { values.attributes.map( + ( { attribute, terms }, index ) => ( + + + + + + ) + ) } + +
AttributeValues
+ { + setValue( + 'attributes[' + + index + + '].attribute', + val + ); + if ( val ) { + focusValueField( + index + ); + } + } } + filteredAttributeIds={ [ + ...selectedAttributeIds, + ...values.attributes + .map( + ( + attr + ) => + attr + ?.attribute + ?.id + ) + .filter( + ( + id + ): id is number => + id !== + undefined + ), + ] } + /> + + + setValue( + 'attributes[' + + index + + '].terms', + val + ) + } + /> + + +
+
+
+ +
+
+ + +
+
+ ); + } } + + { showConfirmClose && ( + setShowConfirmClose( false ) } + onConfirm={ onCancel } + > + { __( + 'You have some attributes added to the list, are you sure you want to cancel?', + 'woocommerce' + ) } + + ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss index 275bc8869c1..b056534865b 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss @@ -41,7 +41,7 @@ padding: 0 $gap-large; &:last-child { - margin: -1px; + margin-bottom: -1px; } } diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx index 67772f3ba66..491ed537c66 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx @@ -2,7 +2,8 @@ * External dependencies */ import { sprintf, __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; +import { Button, Card, CardBody, Popover } from '@wordpress/components'; +import { useState } from '@wordpress/element'; import { ProductAttribute } from '@woocommerce/data'; import { Text } from '@woocommerce/experimental'; import { Sortable, ListItem } from '@woocommerce/components'; @@ -13,6 +14,7 @@ import { closeSmall } from '@wordpress/icons'; */ import './attribute-field.scss'; import AttributeEmptyStateLogo from './attribute-empty-state-logo.svg'; +import { AddAttributeModal } from './add-attribute-modal'; import { reorderSortableProductAttributePositions } from './utils'; type AttributeFieldProps = { @@ -24,6 +26,8 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { value, onChange, } ) => { + const [ showAddAttributeModal, setShowAddAttributeModal ] = + useState( false ); const onRemove = ( attribute: ProductAttribute ) => { // eslint-disable-next-line no-alert if ( window.confirm( __( 'Remove this attribute?', 'woocommerce' ) ) ) { @@ -31,33 +35,69 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { } }; + const onAddNewAttributes = ( newAttributes: ProductAttribute[] ) => { + onChange( [ + ...( value || [] ), + ...newAttributes + .filter( + ( newAttr ) => + ! ( value || [] ).find( + ( attr ) => attr.id === newAttr.id + ) + ) + .map( ( newAttr, index ) => { + newAttr.position = ( value || [] ).length + index; + return newAttr; + } ), + ] ); + setShowAddAttributeModal( false ); + }; + if ( ! value || value.length === 0 ) { return ( -
-
- Completed - - { __( 'No attributes yet', 'woocommerce' ) } - - -
-
+ + +
+
+ Completed + + { __( 'No attributes yet', 'woocommerce' ) } + + +
+ { showAddAttributeModal && ( + + setShowAddAttributeModal( false ) + } + onAdd={ onAddNewAttributes } + selectedAttributeIds={ ( value || [] ).map( + ( attr ) => attr.id + ) } + /> + ) } + +
+
+
); } @@ -127,11 +167,19 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { + { showAddAttributeModal && ( + setShowAddAttributeModal( false ) } + onAdd={ onAddNewAttributes } + selectedAttributeIds={ value.map( ( attr ) => attr.id ) } + /> + ) } +
); }; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx new file mode 100644 index 00000000000..c88173c4fe9 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx @@ -0,0 +1,289 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; +import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { AddAttributeModal } from '../add-attribute-modal'; + +let attributeOnChange: ( val: ProductAttribute ) => void; +jest.mock( '../../attribute-input-field', () => ( { + AttributeInputField: ( { + onChange, + }: { + onChange: ( + value?: Omit< + ProductAttribute, + 'position' | 'visible' | 'variation' + > + ) => void; + } ) => { + attributeOnChange = onChange; + return
attribute_input_field
; + }, +} ) ); +let attributeTermOnChange: ( val: ProductAttributeTerm[] ) => void; +jest.mock( '../../attribute-term-input-field', () => ( { + AttributeTermInputField: ( { + onChange, + disabled, + }: { + onChange: ( value: ProductAttributeTerm[] ) => void; + disabled: boolean; + } ) => { + attributeTermOnChange = onChange; + return ( +
+ attribute_term_input_field: disabled:{ disabled.toString() } +
+ ); + }, +} ) ); + +const attributeList: ProductAttribute[] = [ + { + id: 15, + name: 'Automotive', + position: 0, + visible: true, + variation: false, + options: [ 'test' ], + }, + { + id: 1, + name: 'Color', + position: 2, + visible: true, + variation: true, + options: [ + 'Beige', + 'black', + 'Blue', + 'brown', + 'Gray', + 'Green', + 'mint', + 'orange', + 'pink', + 'Red', + 'white', + 'Yellow', + ], + }, +]; + +const attributeTermList: ProductAttributeTerm[] = [ + { + id: 23, + name: 'XXS', + slug: 'xxs', + description: '', + menu_order: 1, + count: 1, + }, + { + id: 22, + name: 'XS', + slug: 'xs', + description: '', + menu_order: 2, + count: 1, + }, + { + id: 17, + name: 'S', + slug: 's', + description: '', + menu_order: 3, + count: 1, + }, + { + id: 18, + name: 'M', + slug: 'm', + description: '', + menu_order: 4, + count: 1, + }, + { + id: 19, + name: 'L', + slug: 'l', + description: '', + menu_order: 5, + count: 1, + }, +]; + +describe( 'AddAttributeModal', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should render at-least one row with the attribute dropdown fields', () => { + const { queryAllByText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 1 ); + } ); + + it( 'should enable attribute term field once attribute is selected', () => { + const { queryAllByText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + attributeOnChange( attributeList[ 0 ] ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:false' ) + .length + ).toEqual( 1 ); + } ); + + it( 'should allow us to add multiple new rows with the attribute fields', () => { + const { queryAllByText, queryByRole } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 2 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 2 ); + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 3 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 3 ); + } ); + + it( 'should allow us to remove the added fields', () => { + const { queryAllByText, queryByRole, queryAllByLabelText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 3 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 3 ); + + const removeButtons = queryAllByLabelText( 'Remove attribute' ); + + removeButtons[ 0 ].click(); + removeButtons[ 1 ].click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 1 ); + } ); + + it( 'should not allow us to remove all the rows', () => { + const { queryAllByText, queryAllByLabelText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + + const removeButtons = queryAllByLabelText( 'Remove attribute' ); + + removeButtons[ 0 ].click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 1 ); + } ); + + describe( 'onAdd', () => { + it( 'should not return empty attribute rows', () => { + const onAddMock = jest.fn(); + const { queryAllByText, queryByLabelText, queryByRole } = render( + {} } + onAdd={ onAddMock } + selectedAttributeIds={ [] } + /> + ); + + const addAnotherButton = queryByLabelText( + 'Add another attribute' + ); + addAnotherButton?.click(); + addAnotherButton?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( + 3 + ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ) + .length + ).toEqual( 3 ); + queryByRole( 'button', { name: 'Add attributes' } )?.click(); + expect( onAddMock ).toHaveBeenCalledWith( [] ); + } ); + + it( 'should not add attribute if no terms were selected', () => { + const onAddMock = jest.fn(); + const { queryByRole } = render( + {} } + onAdd={ onAddMock } + selectedAttributeIds={ [] } + /> + ); + + attributeOnChange( attributeList[ 0 ] ); + queryByRole( 'button', { name: 'Add attributes' } )?.click(); + expect( onAddMock ).toHaveBeenCalledWith( [] ); + } ); + + it( 'should add attribute with terms as string of options', () => { + const onAddMock = jest.fn(); + const { queryByRole } = render( + {} } + onAdd={ onAddMock } + selectedAttributeIds={ [] } + /> + ); + + attributeOnChange( attributeList[ 0 ] ); + attributeTermOnChange( [ + attributeTermList[ 0 ], + attributeTermList[ 1 ], + ] ); + queryByRole( 'button', { name: 'Add attributes' } )?.click(); + expect( onAddMock ).toHaveBeenCalledWith( [ + { + ...attributeList[ 0 ], + options: [ + attributeTermList[ 0 ].name, + attributeTermList[ 1 ].name, + ], + }, + ] ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx new file mode 100644 index 00000000000..3563992023b --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { Spinner } from '@wordpress/components'; +import { + EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME, + QueryProductAttribute, + ProductAttribute, + WCDataSelector, +} from '@woocommerce/data'; +import { + __experimentalSelectControl as SelectControl, + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; + +type AttributeInputFieldProps = { + value?: ProductAttribute; + onChange: ( + value?: Omit< ProductAttribute, 'position' | 'visible' | 'variation' > + ) => void; + label?: string; + placeholder?: string; + disabled?: boolean; + filteredAttributeIds?: number[]; +}; + +export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { + value, + onChange, + placeholder, + label, + disabled, + filteredAttributeIds = [], +} ) => { + const { attributes, isLoading } = useSelect( ( select: WCDataSelector ) => { + const { getProductAttributes, hasFinishedResolution } = select( + EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME + ); + return { + isLoading: ! hasFinishedResolution( 'getProductAttributes' ), + attributes: getProductAttributes(), + }; + } ); + + const getFilteredItems = ( + allItems: Pick< QueryProductAttribute, 'id' | 'name' >[], + inputValue: string + ) => { + return allItems.filter( + ( item ) => + filteredAttributeIds.indexOf( item.id ) < 0 && + ( item.name || '' ) + .toLowerCase() + .startsWith( inputValue.toLowerCase() ) + ); + }; + const selected: Pick< QueryProductAttribute, 'id' | 'name' > | null = value + ? { + id: value.id, + name: value.name, + } + : null; + + return ( + > + items={ attributes || [] } + label={ label || '' } + disabled={ disabled } + getFilteredItems={ getFilteredItems } + placeholder={ placeholder } + getItemLabel={ ( item ) => item?.name || '' } + getItemValue={ ( item ) => item?.id || '' } + selected={ selected } + onSelect={ ( attribute ) => + onChange( { + id: attribute.id, + name: attribute.name, + options: [], + } ) + } + onRemove={ () => onChange() } + > + { ( { + items: renderItems, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { isLoading ? ( + + ) : ( + renderItems.map( ( item, index: number ) => ( + + { item.name } + + ) ) + ) } + + ); + } } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts new file mode 100644 index 00000000000..6000ad95f49 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts @@ -0,0 +1 @@ +export * from './attribute-input-field'; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx new file mode 100644 index 00000000000..3b38ce81b45 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx @@ -0,0 +1,226 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; +import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { ProductAttribute, QueryProductAttribute } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { AttributeInputField } from '../attribute-input-field'; + +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + useSelect: jest.fn(), +} ) ); + +jest.mock( '@wordpress/components', () => ( { + __esModule: true, + Spinner: () =>
spinner
, +} ) ); + +jest.mock( '@woocommerce/components', () => { + return { + __esModule: true, + __experimentalSelectControlMenu: ( { + children, + }: { + children: JSX.Element; + } ) => children, + __experimentalSelectControlMenuItem: ( { + children, + }: { + children: JSX.Element; + } ) =>
{ children }
, + __experimentalSelectControl: ( { + children, + items, + getFilteredItems, + onSelect, + onRemove, + }: { + children: ( options: { + isOpen: boolean; + items: QueryProductAttribute[]; + getMenuProps: () => Record< string, string >; + getItemProps: () => Record< string, string >; + } ) => JSX.Element; + items: QueryProductAttribute[]; + onSelect: ( item: QueryProductAttribute ) => void; + onRemove: ( item: QueryProductAttribute ) => void; + getFilteredItems: ( + allItems: QueryProductAttribute[], + inputValue: string, + selectedItems: QueryProductAttribute[] + ) => QueryProductAttribute[]; + } ) => { + const [ input, setInput ] = useState( '' ); + return ( +
+ attribute_input_field + + + +
+ { children( { + isOpen: true, + items: getFilteredItems( items, input, [] ), + getMenuProps: () => ( {} ), + getItemProps: () => ( {} ), + } ) } +
+
+ ); + }, + }; +} ); + +const attributeList: ProductAttribute[] = [ + { + id: 15, + name: 'Automotive', + position: 0, + visible: true, + variation: false, + options: [ 'test' ], + }, + { + id: 1, + name: 'Color', + position: 2, + visible: true, + variation: true, + options: [ + 'Beige', + 'black', + 'Blue', + 'brown', + 'Gray', + 'Green', + 'mint', + 'orange', + 'pink', + 'Red', + 'white', + 'Yellow', + ], + }, +]; + +describe( 'AttributeInputField', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should show spinner while attributes are loading', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: true, + attributes: undefined, + } ); + const { queryByText } = render( + + ); + expect( queryByText( 'spinner' ) ).toBeInTheDocument(); + } ); + + it( 'should render attributes when finished loading', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + expect( queryByText( 'spinner' ) ).not.toBeInTheDocument(); + expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); + expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); + } ); + + it( 'should filter out attribute ids passed into filteredAttributeIds', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + expect( queryByText( 'spinner' ) ).not.toBeInTheDocument(); + expect( + queryByText( attributeList[ 0 ].name ) + ).not.toBeInTheDocument(); + expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); + } ); + + it( 'should filter attributes by name case insensitive', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + queryByText( 'Update Input' )?.click(); + expect( + queryByText( attributeList[ 0 ].name ) + ).not.toBeInTheDocument(); + expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); + } ); + + it( 'should filter out attributes ids from filteredAttributeIds', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); + expect( + queryByText( attributeList[ 1 ].name ) + ).not.toBeInTheDocument(); + } ); + + it( 'should trigger onChange when onSelect is triggered with attribute value', () => { + const onChangeMock = jest.fn(); + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + queryByText( 'select attribute' )?.click(); + expect( onChangeMock ).toHaveBeenCalledWith( { + id: attributeList[ 0 ].id, + name: attributeList[ 0 ].name, + options: [], + } ); + } ); + + it( 'should trigger onChange when onRemove is triggered with undefined', () => { + const onChangeMock = jest.fn(); + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + queryByText( 'remove attribute' )?.click(); + expect( onChangeMock ).toHaveBeenCalledWith(); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss new file mode 100644 index 00000000000..b4836d8ce10 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss @@ -0,0 +1,5 @@ +.woocommerce-attribute-term-field { + &__loading-spinner { + padding: 12px 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx new file mode 100644 index 00000000000..5d69aced711 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx @@ -0,0 +1,190 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CheckboxControl, Spinner } from '@wordpress/components'; +import { resolveSelect } from '@wordpress/data'; +import { useCallback, useEffect, useState } from '@wordpress/element'; +import { useDebounce } from '@wordpress/compose'; +import { + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, + ProductAttributeTerm, +} from '@woocommerce/data'; +import { + selectControlStateChangeTypes, + __experimentalSelectControl as SelectControl, + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './attribute-term-input-field.scss'; + +type AttributeTermInputFieldProps = { + value?: ProductAttributeTerm[]; + onChange: ( value: ProductAttributeTerm[] ) => void; + attributeId?: number; + placeholder?: string; + disabled?: boolean; +}; + +export const AttributeTermInputField: React.FC< + AttributeTermInputFieldProps +> = ( { value = [], onChange, placeholder, disabled, attributeId } ) => { + const [ fetchedItems, setFetchedItems ] = useState< + ProductAttributeTerm[] + >( [] ); + const [ isFetching, setIsFetching ] = useState( false ); + + const fetchItems = useCallback( + ( searchString: string | undefined ) => { + setIsFetching( true ); + return resolveSelect( + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME + ) + .getProductAttributeTerms< ProductAttributeTerm[] >( { + search: searchString || '', + attribute_id: attributeId, + } ) + .then( + ( attributeTerms ) => { + setFetchedItems( attributeTerms ); + setIsFetching( false ); + return attributeTerms; + }, + ( error ) => { + setIsFetching( false ); + return error; + } + ); + }, + [ attributeId ] + ); + + const debouncedSearch = useDebounce( fetchItems, 250 ); + + useEffect( () => { + if ( + ! disabled && + attributeId !== undefined && + ! fetchedItems.length + ) { + fetchItems( '' ); + } + }, [ disabled, attributeId ] ); + + const onRemove = ( item: ProductAttributeTerm ) => { + onChange( value.filter( ( opt ) => opt.slug !== item.slug ) ); + }; + + const onSelect = ( item: ProductAttributeTerm ) => { + const isSelected = value.find( ( i ) => i.slug === item.slug ); + if ( isSelected ) { + onRemove( item ); + return; + } + onChange( [ ...value, item ] ); + }; + + const selectedTermSlugs = ( value || [] ).map( ( term ) => term.slug ); + + return ( + + items={ fetchedItems } + multiple + disabled={ disabled || ! attributeId } + label="" + getFilteredItems={ ( allItems ) => allItems } + onInputChange={ debouncedSearch } + placeholder={ placeholder || '' } + getItemLabel={ ( item ) => item?.name || '' } + getItemValue={ ( item ) => item?.slug || '' } + stateReducer={ ( state, actionAndChanges ) => { + const { changes, type } = actionAndChanges; + switch ( type ) { + case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem: + return { + ...changes, + inputValue: state.inputValue, + }; + case selectControlStateChangeTypes.ItemClick: + return { + ...changes, + isOpen: true, + inputValue: state.inputValue, + highlightedIndex: state.highlightedIndex, + }; + default: + return changes; + } + } } + selected={ value } + onSelect={ onSelect } + onRemove={ onRemove } + className="woocommerce-attribute-term-field" + > + { ( { + items, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { [ + isFetching ? ( +
+ +
+ ) : null, + ...items.map( ( item, menuIndex ) => { + const isSelected = selectedTermSlugs.includes( + item.slug + ); + + return ( + + <> + null } + checked={ isSelected } + label={ + + { item.name } + + } + /> + + + ); + } ), + ].filter( + ( child ): child is JSX.Element => child !== null + ) } +
+ ); + } } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts new file mode 100644 index 00000000000..562b73ccb93 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts @@ -0,0 +1 @@ +export * from './attribute-term-input-field'; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx new file mode 100644 index 00000000000..94e9bae3d38 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx @@ -0,0 +1,208 @@ +/** + * External dependencies + */ +import { act, render, waitFor, screen } from '@testing-library/react'; +import { useState } from '@wordpress/element'; +import { resolveSelect } from '@wordpress/data'; +import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { AttributeTermInputField } from '../attribute-term-input-field'; + +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + resolveSelect: jest.fn(), +} ) ); + +jest.mock( '@wordpress/components', () => { + return { + __esModule: true, + Spinner: () =>
spinner
, + }; +} ); + +jest.mock( '@woocommerce/components', () => { + return { + __esModule: true, + __experimentalSelectControlMenu: ( { + children, + }: { + children: JSX.Element; + } ) => children, + __experimentalSelectControlMenuItem: ( { + children, + }: { + children: JSX.Element; + } ) =>
{ children }
, + __experimentalSelectControl: ( { + children, + items, + getFilteredItems, + }: { + children: ( options: { + isOpen: boolean; + items: ProductAttributeTerm[]; + getMenuProps: () => Record< string, string >; + getItemProps: () => Record< string, string >; + } ) => JSX.Element; + items: ProductAttributeTerm[]; + getFilteredItems: ( + allItems: ProductAttributeTerm[], + inputValue: string, + selectedItems: ProductAttributeTerm[] + ) => ProductAttributeTerm[]; + } ) => { + const [ input, setInput ] = useState( '' ); + return ( +
+ attribute_input_field + +
+ { children( { + isOpen: true, + items: getFilteredItems( items, input, [] ), + getMenuProps: () => ( {} ), + getItemProps: () => ( {} ), + } ) } +
+
+ ); + }, + }; +} ); + +const attributeList: ProductAttribute[] = [ + { + id: 15, + name: 'Automotive', + position: 0, + visible: true, + variation: false, + options: [ 'test' ], + }, + { + id: 1, + name: 'Color', + position: 2, + visible: true, + variation: true, + options: [ + 'Beige', + 'black', + 'Blue', + 'brown', + 'Gray', + 'Green', + 'mint', + 'orange', + 'pink', + 'Red', + 'white', + 'Yellow', + ], + }, +]; + +const attributeTermList: ProductAttributeTerm[] = [ + { + id: 23, + name: 'XXS', + slug: 'xxs', + description: '', + menu_order: 1, + count: 1, + }, + { + id: 22, + name: 'XS', + slug: 'xs', + description: '', + menu_order: 2, + count: 1, + }, + { + id: 17, + name: 'S', + slug: 's', + description: '', + menu_order: 3, + count: 1, + }, + { + id: 18, + name: 'M', + slug: 'm', + description: '', + menu_order: 4, + count: 1, + }, + { + id: 19, + name: 'L', + slug: 'l', + description: '', + menu_order: 5, + count: 1, + }, +]; + +describe( 'AttributeTermInputField', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should not trigger resolveSelect if attributeId is not defined', () => { + render( ); + expect( resolveSelect ).not.toHaveBeenCalled(); + } ); + + it( 'should not trigger resolveSelect if attributeId is defined but field disabled', () => { + render( + + ); + expect( resolveSelect ).not.toHaveBeenCalled(); + } ); + + it( 'should trigger resolveSelect if attributeId is defined and field not disabled', () => { + const getProductAttributesMock = jest.fn().mockResolvedValue( [] ); + ( resolveSelect as jest.Mock ).mockReturnValue( { + getProductAttributeTerms: getProductAttributesMock, + } ); + render( + + ); + expect( getProductAttributesMock ).toHaveBeenCalledWith( { + search: '', + attribute_id: 2, + } ); + } ); + + it( 'should render spinner while retrieving products', async () => { + const getProductAttributesMock = jest + .fn() + .mockReturnValue( { then: () => {} } ); + ( resolveSelect as jest.Mock ).mockReturnValue( { + getProductAttributeTerms: getProductAttributesMock, + } ); + await act( async () => { + render( + + ); + } ); + // debug(); + await waitFor( () => { + expect( screen.queryByText( 'spinner' ) ).toBeInTheDocument(); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss b/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss index 09eb2478645..a80d03fe3eb 100644 --- a/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss +++ b/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss @@ -58,9 +58,6 @@ } .woocommerce-experimental-select-control { - &__input { - height: 30px; - } &__combox-box-icon { box-sizing: unset; } diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.scss b/plugins/woocommerce-admin/client/products/product-form-actions.scss index 2a7f384dd8c..724363ad170 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.scss +++ b/plugins/woocommerce-admin/client/products/product-form-actions.scss @@ -1,5 +1,5 @@ -$gutenberg-blue: #007cba; -$gutenberg-blue-darker: #0063a1; +$gutenberg-blue: var(--wp-admin-theme-color); +$gutenberg-blue-darker: var(--wp-admin-theme-color-darker-20); .woocommerce-product-form-actions { display: flex; diff --git a/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx b/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx index cc569e491e3..ecd11453fd1 100644 --- a/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Card, CardBody } from '@wordpress/components'; import { Link, useFormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; @@ -43,11 +42,7 @@ export const AttributesSection: React.FC = () => { } > - - - - - + ); }; diff --git a/plugins/woocommerce/changelog/add-34331_add_attributes_modal b/plugins/woocommerce/changelog/add-34331_add_attributes_modal new file mode 100644 index 00000000000..05ccdce5eb3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34331_add_attributes_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add add attribute modal to the attribute field in the new product management MVP From 4fc33dc3ef69beb2352a2a6bb481da3bcd79abda Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Thu, 20 Oct 2022 13:52:03 +1300 Subject: [PATCH 024/149] Add back the code reference updates action (#35140) Add back the code reference updates action. --- .github/workflows/build-release.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f1455a8085c..ba2b99d4814 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -27,3 +27,19 @@ jobs: asset_path: plugins/woocommerce/woocommerce.zip asset_name: woocommerce.zip asset_content_type: application/zip + + + update-code-reference: + if: github.event.release.prerelease == false && github.event.release.draft == false && github.repository_owner == 'woocommerce' + name: Update Code Reference + needs: build + runs-on: ubuntu-20.04 + steps: + - name: Invoke Code Reference build and deploy workflow + uses: aurelien-baudet/workflow-dispatch@v2 + with: + workflow: GitHub Pages deploy + repo: woocommerce/code-reference + token: ${{ secrets.CUSTOM_GH_TOKEN }} + ref: refs/heads/trunk + inputs: '{ "version": "${{ github.event.release.tag_name }}" }' From 3d78fd24eefcba3ad7fc2a7d1dcff3c48027b189 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:21:11 +0800 Subject: [PATCH 025/149] Prepare Packages for Release (#35205) Automated change: Prep @woocommerce/components for release. Co-authored-by: chihsuan --- packages/js/components/CHANGELOG.md | 49 +++++++++++++++++++ .../changelog/add-28_product_details | 4 -- .../changelog/add-29_product_link_slug | 4 -- .../changelog/add-30_list_price_field | 4 -- .../changelog/add-34331_add_attributes_modal | 4 -- ...d-34331_popover_to_select_control_dropdown | 4 -- .../changelog/add-34333_attribute_list | 4 -- .../changelog/add-34437-gallery-toolbar | 4 -- .../add-34657-add-new-shipping-class | 4 -- packages/js/components/changelog/add-34997 | 4 -- .../changelog/add-34_category_field | 4 -- .../changelog/add-39_image_section_details | 4 -- .../changelog/add-component-datetime-picker | 4 -- ...dd-date-time-picker-control-date-only-mode | 4 -- .../add-date-time-picker-control-to-form | 4 -- .../components/changelog/add-draggable-list | 4 -- .../add-form_input_name_dot_notation | 4 -- .../components/changelog/add-gb-text-editor | 4 -- .../js/components/changelog/add-image-gallery | 4 -- .../components/changelog/add-media-uploader | 4 -- .../add-product-management-description | 4 -- .../changelog/add-product_schedule_sale | 4 -- .../changelog/add-section-component | 4 -- .../changelog/add-sortable-accessibility | 4 -- .../changelog/add-split-dropdown-component | 4 -- .../changelog/dev-bump-pnpm-version-restraint | 4 -- .../changelog/dev-fix-admin-tests-pnpm7 | 4 -- .../changelog/dev-fix-pnpm-version-engines | 4 -- packages/js/components/changelog/fix-34112 | 5 -- .../fix-34584_form_onchange_callback | 4 -- ...ix-date-time-picker-control-ambiguous-date | 4 -- .../fix-date-time-picker-control-debounce | 4 -- ...x-date-time-picker-control-initial-setting | 4 -- .../changelog/fix-enriched-label-storybook | 4 -- .../changelog/fix-export-stepper-props | 6 --- .../changelog/fix-plugin-installer-ts | 4 -- .../fix-revert_on_change_third_param_update | 5 -- .../fix-select-control-extensibility | 4 -- .../changelog/fix-select-control-selection | 4 -- .../changelog/remove-split-dropdown | 4 -- .../js/components/changelog/try-downshift | 4 -- .../changelog/update-64-tag-styling | 4 -- .../changelog/update-date-time-picker-control | 4 -- .../update-date-time-picker-control-onchange | 4 -- .../update-experimental_select_control | 4 -- .../js/components/changelog/update-jest-merge | 4 -- .../update-select-control-input-placement | 4 -- .../update-select-control-tag-location | 4 -- .../js/components/changelog/upgrade-pnpm-7 | 4 -- packages/js/components/package.json | 4 +- pnpm-lock.yaml | 10 ++-- 51 files changed, 56 insertions(+), 203 deletions(-) delete mode 100644 packages/js/components/changelog/add-28_product_details delete mode 100644 packages/js/components/changelog/add-29_product_link_slug delete mode 100644 packages/js/components/changelog/add-30_list_price_field delete mode 100644 packages/js/components/changelog/add-34331_add_attributes_modal delete mode 100644 packages/js/components/changelog/add-34331_popover_to_select_control_dropdown delete mode 100644 packages/js/components/changelog/add-34333_attribute_list delete mode 100644 packages/js/components/changelog/add-34437-gallery-toolbar delete mode 100644 packages/js/components/changelog/add-34657-add-new-shipping-class delete mode 100644 packages/js/components/changelog/add-34997 delete mode 100644 packages/js/components/changelog/add-34_category_field delete mode 100644 packages/js/components/changelog/add-39_image_section_details delete mode 100644 packages/js/components/changelog/add-component-datetime-picker delete mode 100644 packages/js/components/changelog/add-date-time-picker-control-date-only-mode delete mode 100644 packages/js/components/changelog/add-date-time-picker-control-to-form delete mode 100644 packages/js/components/changelog/add-draggable-list delete mode 100644 packages/js/components/changelog/add-form_input_name_dot_notation delete mode 100644 packages/js/components/changelog/add-gb-text-editor delete mode 100644 packages/js/components/changelog/add-image-gallery delete mode 100644 packages/js/components/changelog/add-media-uploader delete mode 100644 packages/js/components/changelog/add-product-management-description delete mode 100644 packages/js/components/changelog/add-product_schedule_sale delete mode 100644 packages/js/components/changelog/add-section-component delete mode 100644 packages/js/components/changelog/add-sortable-accessibility delete mode 100644 packages/js/components/changelog/add-split-dropdown-component delete mode 100644 packages/js/components/changelog/dev-bump-pnpm-version-restraint delete mode 100644 packages/js/components/changelog/dev-fix-admin-tests-pnpm7 delete mode 100644 packages/js/components/changelog/dev-fix-pnpm-version-engines delete mode 100644 packages/js/components/changelog/fix-34112 delete mode 100644 packages/js/components/changelog/fix-34584_form_onchange_callback delete mode 100644 packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date delete mode 100644 packages/js/components/changelog/fix-date-time-picker-control-debounce delete mode 100644 packages/js/components/changelog/fix-date-time-picker-control-initial-setting delete mode 100644 packages/js/components/changelog/fix-enriched-label-storybook delete mode 100644 packages/js/components/changelog/fix-export-stepper-props delete mode 100644 packages/js/components/changelog/fix-plugin-installer-ts delete mode 100644 packages/js/components/changelog/fix-revert_on_change_third_param_update delete mode 100644 packages/js/components/changelog/fix-select-control-extensibility delete mode 100644 packages/js/components/changelog/fix-select-control-selection delete mode 100644 packages/js/components/changelog/remove-split-dropdown delete mode 100644 packages/js/components/changelog/try-downshift delete mode 100644 packages/js/components/changelog/update-64-tag-styling delete mode 100644 packages/js/components/changelog/update-date-time-picker-control delete mode 100644 packages/js/components/changelog/update-date-time-picker-control-onchange delete mode 100644 packages/js/components/changelog/update-experimental_select_control delete mode 100644 packages/js/components/changelog/update-jest-merge delete mode 100644 packages/js/components/changelog/update-select-control-input-placement delete mode 100644 packages/js/components/changelog/update-select-control-tag-location delete mode 100644 packages/js/components/changelog/upgrade-pnpm-7 diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md index d911fd0bd50..39efd837868 100644 --- a/packages/js/components/CHANGELOG.md +++ b/packages/js/components/CHANGELOG.md @@ -2,6 +2,55 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [11.0.0](https://www.npmjs.com/package/@woocommerce/components/v/11.0.0) - 2022-10-20 + +- Patch - Export StepperProps for external usage [#35140] +- Patch - Fixed the initial setting of DateTimePickerControl's input field. [#35140] +- Minor - Fix Enriched-label styles - #34382 [#35140] +- Patch - Fix EnrichedLabel Storybook story styles so they don't affect other stories. [#35140] +- Patch - Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes. [#35140] +- Minor - Fix initially selected items in SelectControl component [#35140] +- Patch - Fix issue with form onChange handler, passing outdated values. [#35140] +- Minor - Add date-only mode to DateTimePickerControl. [#35140] +- Minor - Add disabled option to the Select Control input component and alter the onInputChange callback [#35140] +- Minor - Add form input name dot notation name="product.dimensions.width" [#35140] +- Minor - Add FormSection component [#35140] +- Minor - Add ImageGallery component [#35140] +- Minor - Adding datetimepicker component. [#35140] +- Minor - Adding on-click toolbar to image gallery component items. [#35140] +- Minor - Add label prop to rich text editor [#35140] +- Minor - Add MediaUploader component [#35140] +- Minor - Add rich text editor component [#35140] +- Minor - Add SortableList component [#35140] +- Minor - Allow external tags in SelectControl component [#35140] +- Major [ **BREAKING CHANGE** ] - Create new experimental SelectControl component [#35140] +- Minor - Export ImportProps type. Add DateTimePickerControl to Form stories and tests. [#35140] +- Minor - Images Product management [#35140] +- Minor - Remove EnrichedLabel component in favor of Tooltip component [#35140] +- Minor - Update resetForm arguments, adding changed fields, touched fields and errors. [#35140] +- Minor - [PM Components] Create SplitDropdown component. #34180 [#35140] +- Minor - Add label, placeholder, and help props to DateTimePickerControl. [#35140] +- Minor - Adds setValues support to FormContext [#35140] +- Minor - Add support in SelectControl for using the popover slot for the popover. [#35140] +- Minor - Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift. [#35140] +- Minor - Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options. [#35140] +- Patch - Update tag component styling [#35140] +- Minor - Update text input placement in SelectControl [#35140] +- Minor - Add component EnrichedLabel #34214 [#35140] +- Patch - Add missing type definitions and add babel config for tests [#35140] +- Minor - Add new shippping class modal to a shipping class section in product page [#35140] +- Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#35140] +- Minor - Fix node and pnpm versions via engines [#35140] +- Patch - Merging trunk with local [#35140] +- Patch - Removed unfinished and unused SplitDropdown component. [#35140] +- Minor - Update Plugin installer component to TS [#35140] +- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35140] +- Patch - Assume ambiguous dates passed into DateTimePickerControl are UTC. [#35140] +- Minor - Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). [#35140] +- Patch - Remove default selected sortable item. [#35140] +- Minor - Improve experimental SelectControl accessibility [#35140] +- Minor - Improve Sortable component acessibility [#35140] + ## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12 - Patch - Added in missing TS definitions in package.json [#34279] diff --git a/packages/js/components/changelog/add-28_product_details b/packages/js/components/changelog/add-28_product_details deleted file mode 100644 index 8c362772846..00000000000 --- a/packages/js/components/changelog/add-28_product_details +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Add component EnrichedLabel #34214 diff --git a/packages/js/components/changelog/add-29_product_link_slug b/packages/js/components/changelog/add-29_product_link_slug deleted file mode 100644 index a0e4f41a088..00000000000 --- a/packages/js/components/changelog/add-29_product_link_slug +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Update resetForm arguments, adding changed fields, touched fields and errors. diff --git a/packages/js/components/changelog/add-30_list_price_field b/packages/js/components/changelog/add-30_list_price_field deleted file mode 100644 index 6b6de73827b..00000000000 --- a/packages/js/components/changelog/add-30_list_price_field +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix Enriched-label styles - #34382 diff --git a/packages/js/components/changelog/add-34331_add_attributes_modal b/packages/js/components/changelog/add-34331_add_attributes_modal deleted file mode 100644 index e252934b519..00000000000 --- a/packages/js/components/changelog/add-34331_add_attributes_modal +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add disabled option to the Select Control input component and alter the onInputChange callback diff --git a/packages/js/components/changelog/add-34331_popover_to_select_control_dropdown b/packages/js/components/changelog/add-34331_popover_to_select_control_dropdown deleted file mode 100644 index 20f4ddf6ee3..00000000000 --- a/packages/js/components/changelog/add-34331_popover_to_select_control_dropdown +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Add support in SelectControl for using the popover slot for the popover. diff --git a/packages/js/components/changelog/add-34333_attribute_list b/packages/js/components/changelog/add-34333_attribute_list deleted file mode 100644 index 7655dcde6df..00000000000 --- a/packages/js/components/changelog/add-34333_attribute_list +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Remove default selected sortable item. diff --git a/packages/js/components/changelog/add-34437-gallery-toolbar b/packages/js/components/changelog/add-34437-gallery-toolbar deleted file mode 100644 index d16e53c3f42..00000000000 --- a/packages/js/components/changelog/add-34437-gallery-toolbar +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Adding on-click toolbar to image gallery component items. diff --git a/packages/js/components/changelog/add-34657-add-new-shipping-class b/packages/js/components/changelog/add-34657-add-new-shipping-class deleted file mode 100644 index 3a6e304d072..00000000000 --- a/packages/js/components/changelog/add-34657-add-new-shipping-class +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Add new shippping class modal to a shipping class section in product page diff --git a/packages/js/components/changelog/add-34997 b/packages/js/components/changelog/add-34997 deleted file mode 100644 index 443a11eca4c..00000000000 --- a/packages/js/components/changelog/add-34997 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Remove EnrichedLabel component in favor of Tooltip component diff --git a/packages/js/components/changelog/add-34_category_field b/packages/js/components/changelog/add-34_category_field deleted file mode 100644 index 13d991676ee..00000000000 --- a/packages/js/components/changelog/add-34_category_field +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift. diff --git a/packages/js/components/changelog/add-39_image_section_details b/packages/js/components/changelog/add-39_image_section_details deleted file mode 100644 index ed8d9ab775a..00000000000 --- a/packages/js/components/changelog/add-39_image_section_details +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Images Product management diff --git a/packages/js/components/changelog/add-component-datetime-picker b/packages/js/components/changelog/add-component-datetime-picker deleted file mode 100644 index b41e9de5483..00000000000 --- a/packages/js/components/changelog/add-component-datetime-picker +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Adding datetimepicker component. diff --git a/packages/js/components/changelog/add-date-time-picker-control-date-only-mode b/packages/js/components/changelog/add-date-time-picker-control-date-only-mode deleted file mode 100644 index 0e0d26f415a..00000000000 --- a/packages/js/components/changelog/add-date-time-picker-control-date-only-mode +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add date-only mode to DateTimePickerControl. diff --git a/packages/js/components/changelog/add-date-time-picker-control-to-form b/packages/js/components/changelog/add-date-time-picker-control-to-form deleted file mode 100644 index 951fc571ba5..00000000000 --- a/packages/js/components/changelog/add-date-time-picker-control-to-form +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Export ImportProps type. Add DateTimePickerControl to Form stories and tests. diff --git a/packages/js/components/changelog/add-draggable-list b/packages/js/components/changelog/add-draggable-list deleted file mode 100644 index 470f358d6a5..00000000000 --- a/packages/js/components/changelog/add-draggable-list +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add SortableList component diff --git a/packages/js/components/changelog/add-form_input_name_dot_notation b/packages/js/components/changelog/add-form_input_name_dot_notation deleted file mode 100644 index dadd617537a..00000000000 --- a/packages/js/components/changelog/add-form_input_name_dot_notation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add form input name dot notation name="product.dimensions.width" diff --git a/packages/js/components/changelog/add-gb-text-editor b/packages/js/components/changelog/add-gb-text-editor deleted file mode 100644 index 8ade85318e8..00000000000 --- a/packages/js/components/changelog/add-gb-text-editor +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add rich text editor component diff --git a/packages/js/components/changelog/add-image-gallery b/packages/js/components/changelog/add-image-gallery deleted file mode 100644 index 9c475bb48ed..00000000000 --- a/packages/js/components/changelog/add-image-gallery +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add ImageGallery component diff --git a/packages/js/components/changelog/add-media-uploader b/packages/js/components/changelog/add-media-uploader deleted file mode 100644 index b0418ffd014..00000000000 --- a/packages/js/components/changelog/add-media-uploader +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add MediaUploader component diff --git a/packages/js/components/changelog/add-product-management-description b/packages/js/components/changelog/add-product-management-description deleted file mode 100644 index 7e220824778..00000000000 --- a/packages/js/components/changelog/add-product-management-description +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add label prop to rich text editor diff --git a/packages/js/components/changelog/add-product_schedule_sale b/packages/js/components/changelog/add-product_schedule_sale deleted file mode 100644 index c745e217882..00000000000 --- a/packages/js/components/changelog/add-product_schedule_sale +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Adds setValues support to FormContext diff --git a/packages/js/components/changelog/add-section-component b/packages/js/components/changelog/add-section-component deleted file mode 100644 index 7277b31d330..00000000000 --- a/packages/js/components/changelog/add-section-component +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add FormSection component diff --git a/packages/js/components/changelog/add-sortable-accessibility b/packages/js/components/changelog/add-sortable-accessibility deleted file mode 100644 index 407bba232b4..00000000000 --- a/packages/js/components/changelog/add-sortable-accessibility +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Improve Sortable component acessibility diff --git a/packages/js/components/changelog/add-split-dropdown-component b/packages/js/components/changelog/add-split-dropdown-component deleted file mode 100644 index 5c63071d0fc..00000000000 --- a/packages/js/components/changelog/add-split-dropdown-component +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -[PM Components] Create SplitDropdown component. #34180 diff --git a/packages/js/components/changelog/dev-bump-pnpm-version-restraint b/packages/js/components/changelog/dev-bump-pnpm-version-restraint deleted file mode 100644 index f7511cb6974..00000000000 --- a/packages/js/components/changelog/dev-bump-pnpm-version-restraint +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues diff --git a/packages/js/components/changelog/dev-fix-admin-tests-pnpm7 b/packages/js/components/changelog/dev-fix-admin-tests-pnpm7 deleted file mode 100644 index d8b487150a2..00000000000 --- a/packages/js/components/changelog/dev-fix-admin-tests-pnpm7 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Add missing type definitions and add babel config for tests diff --git a/packages/js/components/changelog/dev-fix-pnpm-version-engines b/packages/js/components/changelog/dev-fix-pnpm-version-engines deleted file mode 100644 index a1804a282f0..00000000000 --- a/packages/js/components/changelog/dev-fix-pnpm-version-engines +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Fix node and pnpm versions via engines diff --git a/packages/js/components/changelog/fix-34112 b/packages/js/components/changelog/fix-34112 deleted file mode 100644 index 2c06f8f49fb..00000000000 --- a/packages/js/components/changelog/fix-34112 +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: tweak -Comment: Minor update of react and react-dom to 17.0.2. - - diff --git a/packages/js/components/changelog/fix-34584_form_onchange_callback b/packages/js/components/changelog/fix-34584_form_onchange_callback deleted file mode 100644 index a060d4c1b74..00000000000 --- a/packages/js/components/changelog/fix-34584_form_onchange_callback +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix issue with form onChange handler, passing outdated values. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date b/packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date deleted file mode 100644 index 113cac85c15..00000000000 --- a/packages/js/components/changelog/fix-date-time-picker-control-ambiguous-date +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Assume ambiguous dates passed into DateTimePickerControl are UTC. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-debounce b/packages/js/components/changelog/fix-date-time-picker-control-debounce deleted file mode 100644 index e134508881a..00000000000 --- a/packages/js/components/changelog/fix-date-time-picker-control-debounce +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-initial-setting b/packages/js/components/changelog/fix-date-time-picker-control-initial-setting deleted file mode 100644 index 589414fa14e..00000000000 --- a/packages/js/components/changelog/fix-date-time-picker-control-initial-setting +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixed the initial setting of DateTimePickerControl's input field. diff --git a/packages/js/components/changelog/fix-enriched-label-storybook b/packages/js/components/changelog/fix-enriched-label-storybook deleted file mode 100644 index 05370ad6e76..00000000000 --- a/packages/js/components/changelog/fix-enriched-label-storybook +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix EnrichedLabel Storybook story styles so they don't affect other stories. diff --git a/packages/js/components/changelog/fix-export-stepper-props b/packages/js/components/changelog/fix-export-stepper-props deleted file mode 100644 index 7d1cda9f63f..00000000000 --- a/packages/js/components/changelog/fix-export-stepper-props +++ /dev/null @@ -1,6 +0,0 @@ -Significance: patch -Type: fix - -Export StepperProps for external usage - - diff --git a/packages/js/components/changelog/fix-plugin-installer-ts b/packages/js/components/changelog/fix-plugin-installer-ts deleted file mode 100644 index 623f429f0e2..00000000000 --- a/packages/js/components/changelog/fix-plugin-installer-ts +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update Plugin installer component to TS diff --git a/packages/js/components/changelog/fix-revert_on_change_third_param_update b/packages/js/components/changelog/fix-revert_on_change_third_param_update deleted file mode 100644 index cd8bd7ed107..00000000000 --- a/packages/js/components/changelog/fix-revert_on_change_third_param_update +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: tweak -Comment: Reverted change of last PR as part of #34614 - - diff --git a/packages/js/components/changelog/fix-select-control-extensibility b/packages/js/components/changelog/fix-select-control-extensibility deleted file mode 100644 index 40dee5ca1b0..00000000000 --- a/packages/js/components/changelog/fix-select-control-extensibility +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Improve experimental SelectControl accessibility diff --git a/packages/js/components/changelog/fix-select-control-selection b/packages/js/components/changelog/fix-select-control-selection deleted file mode 100644 index 438b5c29632..00000000000 --- a/packages/js/components/changelog/fix-select-control-selection +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix initially selected items in SelectControl component diff --git a/packages/js/components/changelog/remove-split-dropdown b/packages/js/components/changelog/remove-split-dropdown deleted file mode 100644 index 87f296ba40f..00000000000 --- a/packages/js/components/changelog/remove-split-dropdown +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Removed unfinished and unused SplitDropdown component. diff --git a/packages/js/components/changelog/try-downshift b/packages/js/components/changelog/try-downshift deleted file mode 100644 index 38315113e38..00000000000 --- a/packages/js/components/changelog/try-downshift +++ /dev/null @@ -1,4 +0,0 @@ -Significance: major -Type: add - -Create new experimental SelectControl component diff --git a/packages/js/components/changelog/update-64-tag-styling b/packages/js/components/changelog/update-64-tag-styling deleted file mode 100644 index a3bd088afd8..00000000000 --- a/packages/js/components/changelog/update-64-tag-styling +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update tag component styling diff --git a/packages/js/components/changelog/update-date-time-picker-control b/packages/js/components/changelog/update-date-time-picker-control deleted file mode 100644 index 20911f9f710..00000000000 --- a/packages/js/components/changelog/update-date-time-picker-control +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Add label, placeholder, and help props to DateTimePickerControl. diff --git a/packages/js/components/changelog/update-date-time-picker-control-onchange b/packages/js/components/changelog/update-date-time-picker-control-onchange deleted file mode 100644 index 21e87a4f6b3..00000000000 --- a/packages/js/components/changelog/update-date-time-picker-control-onchange +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: tweak - -Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). diff --git a/packages/js/components/changelog/update-experimental_select_control b/packages/js/components/changelog/update-experimental_select_control deleted file mode 100644 index d64304cd04c..00000000000 --- a/packages/js/components/changelog/update-experimental_select_control +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options. diff --git a/packages/js/components/changelog/update-jest-merge b/packages/js/components/changelog/update-jest-merge deleted file mode 100644 index 3ffb0e90b2a..00000000000 --- a/packages/js/components/changelog/update-jest-merge +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Merging trunk with local diff --git a/packages/js/components/changelog/update-select-control-input-placement b/packages/js/components/changelog/update-select-control-input-placement deleted file mode 100644 index 8fd514c91c5..00000000000 --- a/packages/js/components/changelog/update-select-control-input-placement +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update text input placement in SelectControl diff --git a/packages/js/components/changelog/update-select-control-tag-location b/packages/js/components/changelog/update-select-control-tag-location deleted file mode 100644 index 4d8382fc1f8..00000000000 --- a/packages/js/components/changelog/update-select-control-tag-location +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Allow external tags in SelectControl component diff --git a/packages/js/components/changelog/upgrade-pnpm-7 b/packages/js/components/changelog/upgrade-pnpm-7 deleted file mode 100644 index 10ee28d636f..00000000000 --- a/packages/js/components/changelog/upgrade-pnpm-7 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Adjust build/test scripts to remove -- -- that was required for pnpm 6. diff --git a/packages/js/components/package.json b/packages/js/components/package.json index d30756a9036..d6dc1577e07 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/components", - "version": "10.3.0", + "version": "11.0.0", "description": "UI components for WooCommerce.", "author": "Automattic", "license": "GPL-3.0-or-later", @@ -161,4 +161,4 @@ "pnpm test-staged" ] } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89a4e9ad051..c427cea03fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21285,7 +21285,7 @@ packages: normalize-path: 3.0.0 schema-utils: 4.0.0 serialize-javascript: 6.0.0 - webpack: 5.70.0_webpack-cli@4.9.2 + webpack: 5.70.0 dev: true /core-js-compat/3.19.1: @@ -21625,7 +21625,7 @@ packages: postcss-modules-values: 4.0.0_postcss@8.4.12 postcss-value-parser: 4.2.0 semver: 7.3.5 - webpack: 5.70.0_webpack-cli@4.9.2 + webpack: 5.70.0 dev: true /css-select-base-adapter/0.1.1: @@ -37459,7 +37459,7 @@ packages: sass: 1.49.9 schema-utils: 3.1.1 semver: 7.3.5 - webpack: 5.70.0_webpack-cli@4.9.2 + webpack: 5.70.0 dev: true /sass-loader/10.2.1_webpack@5.70.0: @@ -39402,7 +39402,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.8.0 - webpack: 5.70.0_webpack-cli@4.9.2 + webpack: 5.70.0 transitivePeerDependencies: - acorn @@ -40593,7 +40593,7 @@ packages: loader-utils: 1.4.0 mime: 2.5.2 schema-utils: 1.0.0 - webpack: 5.70.0_webpack-cli@4.9.2 + webpack: 5.70.0 dev: true /url-loader/3.0.0_webpack@4.46.0: From 80cbb9dcdc3b49315bc3e061a7cf32a073ed8f59 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 20 Oct 2022 13:22:22 +0530 Subject: [PATCH 026/149] Use correct datastore when backfilling orders. (#35176) * Use correct datastore when backfilling orders. * Fix some unit tests and simplify calls. --- .../changelog/fix-refunds_backfill | 4 ++ .../includes/abstracts/abstract-wc-order.php | 12 ++++ .../includes/class-wc-order-factory.php | 2 +- .../Orders/OrdersTableDataStore.php | 61 ++++++++++++------- .../Orders/OrdersTableDataStoreTests.php | 6 +- .../OrdersTableRefundDataStoreTests.php | 28 +++++++++ 6 files changed, 87 insertions(+), 26 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-refunds_backfill diff --git a/plugins/woocommerce/changelog/fix-refunds_backfill b/plugins/woocommerce/changelog/fix-refunds_backfill new file mode 100644 index 00000000000..c79dfc94309 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-refunds_backfill @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Use correct datastore when backfilling orders. diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index 74069a01f2d..c3f031fe34f 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -539,6 +539,18 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { return wc_string_to_bool( $this->get_prop( 'recorded_coupon_usage_counts', $context ) ); } + /** + * Get basic order data in array format. + * + * @return array + */ + public function get_base_data() { + return array_merge( + array( 'id' => $this->get_id() ), + $this->data + ); + } + /* |-------------------------------------------------------------------------- | Setters diff --git a/plugins/woocommerce/includes/class-wc-order-factory.php b/plugins/woocommerce/includes/class-wc-order-factory.php index 94e01d390e7..3d023d6a7fb 100644 --- a/plugins/woocommerce/includes/class-wc-order-factory.php +++ b/plugins/woocommerce/includes/class-wc-order-factory.php @@ -187,7 +187,7 @@ class WC_Order_Factory { * * @return array Array of order_id => class_name. */ - private static function get_class_names_for_order_ids( $order_ids ) { + public static function get_class_names_for_order_ids( $order_ids ) { $order_data_store = WC_Data_Store::load( 'order' ); if ( $order_data_store->has_callable( 'get_orders_type' ) ) { $order_types = $order_data_store->get_orders_type( $order_ids ); diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index a3c11b4f816..7729691a62d 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -497,9 +497,9 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return \WC_Order_Data_Store_CPT Data store instance. */ - private function get_cpt_data_store_instance() { + public function get_cpt_data_store_instance() { if ( ! isset( $this->cpt_data_store ) ) { - $this->cpt_data_store = new \WC_Order_Data_Store_CPT(); + $this->cpt_data_store = $this->get_post_data_store_for_backfill(); } return $this->cpt_data_store; } @@ -985,7 +985,7 @@ WHERE $data_sync_enabled = $data_synchronizer->data_sync_is_enabled() && 0 === $data_synchronizer->get_current_orders_pending_sync_count_cached(); $load_posts_for = array_diff( $order_ids, self::$reading_order_ids ); - $post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( $load_posts_for ) : array(); + $post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array(); foreach ( $data as $order_data ) { $order_id = absint( $order_data->id ); @@ -1084,31 +1084,46 @@ WHERE /** * Helper function to get posts data for an order in bullk. We use to this to compute posts object in bulk so that we can compare it with COT data. * - * @param array $order_ids List of order IDs. + * @param array $orders List of orders mapped by $order_id. * * @return array List of posts. */ - private function get_post_orders_for_ids( array $order_ids ): array { - $cpt_data_store = $this->get_cpt_data_store_instance(); + private function get_post_orders_for_ids( array $orders ): array { + $order_ids = array_keys( $orders ); // We have to bust meta cache, otherwise we will just get the meta cached by OrderTableDataStore. foreach ( $order_ids as $order_id ) { wp_cache_delete( WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' ); } - $query_vars = array( - 'include' => $order_ids, - 'type' => wc_get_order_types(), - 'status' => 'any', - 'limit' => count( $order_ids ), - ); - $cpt_data_store->prime_caches_for_orders( $order_ids, $query_vars ); - $orders = array(); - foreach ( $order_ids as $order_id ) { - $order = new WC_Order(); - $order->set_id( $order_id ); - $cpt_data_store->read( $order ); - $orders[ $order_id ] = $order; + + $cpt_stores = array(); + $cpt_store_orders = array(); + foreach ( $orders as $order_id => $order ) { + $table_data_store = $order->get_data_store(); + $cpt_data_store = $table_data_store->get_cpt_data_store_instance(); + $cpt_store_class_name = get_class( $cpt_data_store ); + if ( ! isset( $cpt_stores[ $cpt_store_class_name ] ) ) { + $cpt_stores[ $cpt_store_class_name ] = $cpt_data_store; + $cpt_store_orders[ $cpt_store_class_name ] = array(); + } + $cpt_store_orders[ $cpt_store_class_name ][ $order_id ] = $order; } - return $orders; + + $cpt_orders = array(); + foreach ( $cpt_stores as $cpt_store_name => $cpt_store ) { + // Prime caches if we can. + if ( method_exists( $cpt_store, 'prime_caches_for_orders' ) ) { + $cpt_store->prime_caches_for_orders( array_keys( $cpt_store_orders[ $cpt_store_name ] ), array() ); + } + + foreach ( $cpt_store_orders[ $cpt_store_name ] as $order_id => $order ) { + $cpt_order_class_name = wc_get_order_type( $order->get_type() )['class_name']; + $cpt_order = new $cpt_order_class_name(); + $cpt_order->set_id( $order_id ); + $cpt_store->read( $cpt_order ); + $cpt_orders[ $order_id ] = $cpt_order; + } + } + return $cpt_orders; } /** @@ -1221,12 +1236,12 @@ WHERE /** * Migrate post record from a given order object. * - * @param \WC_Order $order Order object. - * @param \WC_Order $post_order Order object read from posts. + * @param \WC_Abstract_Order $order Order object. + * @param \WC_Abstract_Order $post_order Order object read from posts. * * @return void */ - private function migrate_post_record( \WC_Order &$order, \WC_Order $post_order ): void { + private function migrate_post_record( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ): void { $this->migrate_meta_data_from_post_order( $order, $post_order ); $post_order_base_data = $post_order->get_base_data(); foreach ( $post_order_base_data as $key => $value ) { diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 15883632971..7625a57d01c 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -1120,7 +1120,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->enable_cot_sync(); $order = $this->create_complex_cot_order(); $post_order_comparison_closure = function ( $order ) { - $post_order = $this->get_post_orders_for_ids( array( $order->get_id() ) )[ $order->get_id() ]; + $post_order = $this->get_post_orders_for_ids( array( $order->get_id() => $order ) )[ $order->get_id() ]; return $this->is_post_different_from_order( $order, $post_order ); }; @@ -1135,6 +1135,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $r_order = new WC_Order(); $r_order->set_id( $order->get_id() ); + $this->switch_data_store( $r_order, $this->sut ); // Reading again will make a call to migrate_post_record. $this->sut->read( $r_order ); $this->assertFalse( $post_order_comparison_closure->call( $this->sut, $r_order ) ); @@ -1806,6 +1807,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * @testDox Test that multiple calls to read don't try to sync again. */ public function test_read_multiple_dont_sync_again_for_same_order() { + $this->toggle_cot( true ); + $this->enable_cot_sync(); $order = $this->create_complex_cot_order(); $order_id = $order->get_id(); @@ -1814,7 +1817,6 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { return $this->should_sync_order( $order ); }; - $this->enable_cot_sync(); $order = new WC_Order(); $order->set_id( $order_id ); $orders = array( $order_id => $order ); diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php index d720011f887..21bb0ddd481 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php @@ -12,6 +12,7 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper; * Class OrdersTableRefundDataStoreTests. */ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case { + use \Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait; /** * @var PostsToOrdersMigrationController @@ -73,4 +74,31 @@ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case { $this->assertEquals( 'Test', $refreshed_refund->get_reason() ); } + /** + * @testDox Test that refunds can be backfilled correctly. + */ + public function test_refunds_backfill() { + $this->enable_cot_sync(); + $this->toggle_cot( true ); + $order = OrderHelper::create_complex_data_store_order( $this->order_data_store ); + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'amount' => 10, + 'reason' => 'Test', + ) + ); + $refund->save(); + $this->assertTrue( $refund->get_id() > 0 ); + + // Check that data was saved. + $refreshed_refund = new WC_Order_Refund(); + $cpt_store = $this->sut->get_cpt_data_store_instance(); + $refreshed_refund->set_id( $refund->get_id() ); + $cpt_store->read( $refreshed_refund ); + $this->assertEquals( $refund->get_id(), $refreshed_refund->get_id() ); + $this->assertEquals( 10, $refreshed_refund->get_amount() ); + $this->assertEquals( 'Test', $refreshed_refund->get_reason() ); + } + } From 8848e4aa47e0561ef98a37f2dc789fd0e726b6b3 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 20 Oct 2022 14:47:58 +0530 Subject: [PATCH 027/149] Check before getting order classname to see if it exists. (#35207) --- plugins/woocommerce/changelog/fix-warning_order | 4 ++++ .../woocommerce/includes/class-wc-order-factory.php | 2 +- .../DataStores/Orders/OrdersTableDataStoreTests.php | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-warning_order diff --git a/plugins/woocommerce/changelog/fix-warning_order b/plugins/woocommerce/changelog/fix-warning_order new file mode 100644 index 00000000000..f0f37f3dce1 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-warning_order @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Check whether order has classname before returning. diff --git a/plugins/woocommerce/includes/class-wc-order-factory.php b/plugins/woocommerce/includes/class-wc-order-factory.php index 3d023d6a7fb..bc096b0016d 100644 --- a/plugins/woocommerce/includes/class-wc-order-factory.php +++ b/plugins/woocommerce/includes/class-wc-order-factory.php @@ -233,7 +233,7 @@ class WC_Order_Factory { */ private static function get_class_name_for_order_id( $order_id ) { $classname = self::get_class_names_for_order_ids( array( $order_id ) ); - return $classname[ $order_id ]; + return $classname[ $order_id ] ?? false; } } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 7625a57d01c..5e38ff264cb 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -1824,4 +1824,15 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->sut->read_multiple( $orders ); $this->assertFalse( $should_sync_callable->call( $this->sut, $order ) ); } + + /** + * @testDox Make sure get_order return false when checking an order of different order types without warning. + */ + public function test_get_order_with_id_for_different_type() { + $this->toggle_cot( true ); + $this->disable_cot_sync(); + $product = new \WC_Product(); + $product->save(); + $this->assertFalse( wc_get_order( $product->get_id() ) ); + } } From cc0c4ef44706f8bfb1a9df41382c6e52d586abb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Thu, 20 Oct 2022 12:04:37 +0200 Subject: [PATCH 028/149] Improve the warnings about incompatibilites between plugins and features (#35198) * Use AccessiblePrivateMethods in DataSynchronizer. Also fix some code styling stuff. * Introduce the woocommerce_feature_description_tip filter. ...and use it to display a warning for the COT feature if there are orders pending sync. * Display the plugin-feature incompatibility warning in all admin pages. ...except in the plugins list when we are already showing the "You are viewing plugins with incompatibilities" page. Also change the styling from warning to error. * Add changelog file * Revert accidental change. Co-authored-by: Vedanshu Jain --- ...rove-plugin-feature-compatibility-warnings | 4 + .../DataStores/Orders/DataSynchronizer.php | 129 ++++++++++++++---- .../Internal/Features/FeaturesController.php | 45 +++--- 3 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings diff --git a/plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings b/plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings new file mode 100644 index 00000000000..1217a434640 --- /dev/null +++ b/plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Improve the warnings about incompatibilities between plugins and features diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index d9551175a09..39b9b02ca02 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders; use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface; +use Automattic\WooCommerce\Internal\Features\FeaturesController; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; use Automattic\WooCommerce\Proxies\LegacyProxy; @@ -20,6 +22,8 @@ defined( 'ABSPATH' ) || exit; */ class DataSynchronizer implements BatchProcessorInterface { + use AccessiblePrivateMethods; + public const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled'; private const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count'; public const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished'; @@ -63,28 +67,9 @@ class DataSynchronizer implements BatchProcessorInterface { * Class constructor. */ public function __construct() { - // When posts is authoritative and sync is enabled, deleting a post also deletes COT data. - add_action( - 'deleted_post', - function( $postid, $post ) { - if ( 'shop_order' === $post->post_type && ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { - $this->data_store->delete_order_data_from_custom_order_tables( $postid ); - } - }, - 10, - 2 - ); - - // When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table. - add_action( - 'woocommerce_update_order', - function ( $order_id ) { - if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { - $this->posts_to_cot_migrator->migrate_orders( array( $order_id ) ); - } - }, - 100 - ); + self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 ); + self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 ); + self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 ); } /** @@ -93,7 +78,8 @@ class DataSynchronizer implements BatchProcessorInterface { * @param OrdersTableDataStore $data_store The data store to use. * @param DatabaseUtil $database_util The database util class to use. * @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use. - *@internal + * @param LegacyProxy $legacy_proxy The legacy proxy instance to use. + * @internal */ final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util, PostsToOrdersMigrationController $posts_to_cot_migrator, LegacyProxy $legacy_proxy ) { $this->data_store = $data_store; @@ -266,10 +252,10 @@ SELECT( $order_post_types = wc_get_order_types( 'cot-migration' ); $order_post_type_placeholders = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared switch ( $type ) { case self::ID_TYPE_MISSING_IN_ORDERS_TABLE: - $sql = $wpdb->prepare( - " + $sql = $wpdb->prepare(" SELECT posts.ID FROM $wpdb->posts posts LEFT JOIN $orders_table orders ON posts.ID = orders.id WHERE @@ -305,6 +291,7 @@ WHERE default: throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' ); } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:ignore WordPress.DB return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) ); @@ -412,4 +399,96 @@ WHERE public function get_description(): string { return 'Synchronizes orders between posts and custom order tables.'; } + + /** + * Handle the 'deleted_post' action. + * + * When posts is authoritative and sync is enabled, deleting a post also deletes COT data. + * + * @param int $postid The post id. + * @param WP_Post $post The deleted post. + */ + private function handle_deleted_post( $postid, $post ): void { + if ( 'shop_order' === $post->post_type && ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { + $this->data_store->delete_order_data_from_custom_order_tables( $postid ); + } + } + + /** + * Handle the 'woocommerce_update_order' action. + * + * When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table. + * + * @param int $order_id The order id. + */ + private function handle_updated_order( $order_id ): void { + if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { + $this->posts_to_cot_migrator->migrate_orders( array( $order_id ) ); + } + } + + /** + * Handle the 'woocommerce_feature_description_tip' filter. + * + * When the COT feature is enabled and there are orders pending sync (in either direction), + * show a "you should ync before disabling" warning under the feature in the features page. + * Skip this if the UI prevents changing the feature enable status. + * + * @param string $desc_tip The original description tip for the feature. + * @param string $feature_id The feature id. + * @param bool $ui_disabled True if the UI doesn't allow to enable or disable the feature. + * @return string The new description tip for the feature. + */ + private function handle_feature_description_tip( $desc_tip, $feature_id, $ui_disabled ): string { + if ( 'custom_order_tables' !== $feature_id || $ui_disabled ) { + return $desc_tip; + } + + $features_controller = wc_get_container()->get( FeaturesController::class ); + $feature_is_enabled = $features_controller->feature_is_enabled( 'custom_order_tables' ); + if ( ! $feature_is_enabled ) { + return $desc_tip; + } + + $pending_sync_count = $this->get_current_orders_pending_sync_count(); + if ( ! $pending_sync_count ) { + return $desc_tip; + } + + if ( $this->custom_orders_table_is_authoritative() ) { + $extra_tip = sprintf( + _n( + "⚠ There's one order pending sync from the orders table to the posts table. The feature shouldn't be disabled until this order is synchronized.", + "⚠ There are %1\$d orders pending sync from the orders table to the posts table. The feature shouldn't be disabled until these orders are synchronized.", + $pending_sync_count, + 'woocommerce' + ), + $pending_sync_count + ); + } else { + $extra_tip = sprintf( + _n( + "⚠ There's one order pending sync from the posts table to the orders table. The feature shouldn't be disabled until this order is synchronized.", + "⚠ There are %%1\$d orders pending sync from the posts table to the orders table. The feature shouldn't be disabled until these orders are synchronized.", + $pending_sync_count, + 'woocommerce' + ), + $pending_sync_count + ); + } + + $cot_settings_url = add_query_arg( + array( + 'page' => 'wc-settings', + 'tab' => 'advanced', + 'section' => 'custom_data_stores', + ), + admin_url( 'admin.php' ) + ); + + /* translators: %s = URL of the custom data stores settings page */ + $manage_cot_settings_link = sprintf( __( "Manage orders synchronization", 'woocommerce' ), $cot_settings_url ); + + return $desc_tip ? "{$desc_tip}
{$extra_tip} {$manage_cot_settings_link}" : "{$extra_tip} {$manage_cot_settings_link}"; + } } diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 4339f86d16d..92292e30ee0 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -656,6 +656,18 @@ class FeaturesController { } } + /** + * Filter to customize the description tip that appears under the description of each feature in the features settings page. + * + * @since 7.1.0 + * + * @param string $desc_tip The original description tip. + * @param string $feature_id The id of the feature for which the description tip is being customized. + * @param bool $disabled True if the UI currently prevents changing the enable/disable status of the feature. + * @return string The new description tip to use. + */ + $desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled ); + return array( 'title' => $feature['name'], 'desc' => $description, @@ -743,25 +755,18 @@ class FeaturesController { return; } - if ( 'plugins' !== get_current_screen()->id ) { - return; + $feature_filter_description_shown = $this->maybe_display_current_feature_filter_description(); + if ( ! $feature_filter_description_shown ) { + $this->maybe_display_feature_incompatibility_warning(); } - - $this->maybe_display_feature_incompatibility_warning(); - $this->maybe_display_current_feature_filter_description(); } /** - * Shows a warning (on the plugins screen) when there are any incompatibility between active plugins and enabled - * features. + * Shows a warning when there are any incompatibility between active plugins and enabled features. + * The warning is shown in on any admin screen except the plugins screen itself, since + * there's already a "You are viewing */ private function maybe_display_feature_incompatibility_warning(): void { - $plugin_status = $_GET['plugin_status'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput - - if ( ! in_array( $plugin_status, array( '', 'all', 'active' ), true ) ) { - return; - } - $incompatible_plugins = false; foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) { @@ -789,7 +794,7 @@ class FeaturesController { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped ?> -
+

'. */ - private function maybe_display_current_feature_filter_description(): void { + private function maybe_display_current_feature_filter_description(): bool { + if ( 'plugins' !== get_current_screen()->id ) { + return false; + } + // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput $plugin_status = $_GET['plugin_status'] ?? ''; $feature_id = $_GET['feature_id'] ?? ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput if ( 'incompatible_with_feature' !== $plugin_status ) { - return; + return false; } $feature_id = ( '' === $feature_id ) ? 'all' : $feature_id; if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) { - return; + return false; } // phpcs:enable WordPress.Security.NonceVerification @@ -844,6 +853,8 @@ class FeaturesController {
Date: Thu, 20 Oct 2022 15:08:53 +0200 Subject: [PATCH 029/149] Added default additional content to 'woocommerce_email_additional_content_' filter (#35195) * Added default additional content to 'woocommerce_email_additional_content_' . $this->id filter * Changelog. Co-authored-by: Mattia Lerda --- plugins/woocommerce/changelog/pr-29985 | 4 ++++ plugins/woocommerce/includes/emails/class-wc-email.php | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/pr-29985 diff --git a/plugins/woocommerce/changelog/pr-29985 b/plugins/woocommerce/changelog/pr-29985 new file mode 100644 index 00000000000..09bc666d6f0 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-29985 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Added default additional content to emails via filter woocommerce_email_additional_content_. diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php index fdf6605e797..aaa758fe4bb 100644 --- a/plugins/woocommerce/includes/emails/class-wc-email.php +++ b/plugins/woocommerce/includes/emails/class-wc-email.php @@ -400,9 +400,7 @@ class WC_Email extends WC_Settings_API { * @return string */ public function get_additional_content() { - $content = $this->get_option( 'additional_content', '' ); - - return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this ); + return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option( 'additional_content', $this->get_default_additional_content() ) ), $this->object, $this ); } /** From c2be5a6253abd6a5e9d5027b670bed12ffabbd4c Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Thu, 20 Oct 2022 09:23:29 -0700 Subject: [PATCH 030/149] Add untrash order hook (COT/HPOS). (#35087) --- .../changelog/fix-35018-untrash-order-hook | 4 ++++ .../DataStores/Orders/OrdersTableDataStore.php | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-35018-untrash-order-hook diff --git a/plugins/woocommerce/changelog/fix-35018-untrash-order-hook b/plugins/woocommerce/changelog/fix-35018-untrash-order-hook new file mode 100644 index 00000000000..65546d1d723 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35018-untrash-order-hook @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Introduces new hook `woocommerce_untrash_order` as a COT/HPOS analog to `untrash_post`. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 7729691a62d..7d94b7025b2 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1838,6 +1838,16 @@ FROM $order_meta_table return false; } + /** + * Fires before an order is restored from the trash. + * + * @since 7.2.0 + * + * @param int $order_id Order ID. + * @param string $previous_status The status of the order before it was trashed. + */ + do_action( 'woocommerce_untrash_order', $order->get_id(), $previous_status ); + $order->set_status( $previous_status ); $order->save(); @@ -1845,6 +1855,7 @@ FROM $order_meta_table if ( $previous_status === $order->get_status() ) { $order->delete_meta_data( '_wp_trash_meta_status' ); $order->delete_meta_data( '_wp_trash_meta_time' ); + return true; } From dda264cc6de329bd7c3cb8417b4b73e0d5bb9b28 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:10:10 -0500 Subject: [PATCH 031/149] Delete changelog files based on PR 35198 (#35222) Delete changelog files for 35198 Co-authored-by: WooCommerce Bot --- .../changelog/improve-plugin-feature-compatibility-warnings | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings diff --git a/plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings b/plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings deleted file mode 100644 index 1217a434640..00000000000 --- a/plugins/woocommerce/changelog/improve-plugin-feature-compatibility-warnings +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Improve the warnings about incompatibilities between plugins and features From 4831292bb512fd170c0d8de54b96a1c0043cdb84 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:12:54 -0500 Subject: [PATCH 032/149] Delete changelog files based on PR 35176 (#35224) Delete changelog files for 35176 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-refunds_backfill | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-refunds_backfill diff --git a/plugins/woocommerce/changelog/fix-refunds_backfill b/plugins/woocommerce/changelog/fix-refunds_backfill deleted file mode 100644 index c79dfc94309..00000000000 --- a/plugins/woocommerce/changelog/fix-refunds_backfill +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Use correct datastore when backfilling orders. From 23072411f3b0a40e5dcc89dfabc150735383014d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:16:11 -0500 Subject: [PATCH 033/149] Delete changelog files based on PR 35121 (#35226) Delete changelog files for 35121 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-34989 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-34989 diff --git a/plugins/woocommerce/changelog/fix-34989 b/plugins/woocommerce/changelog/fix-34989 deleted file mode 100644 index 32a14ff79bb..00000000000 --- a/plugins/woocommerce/changelog/fix-34989 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add billing and shipping address indexes on order update. From 17cf0055b59ebf7a8c541833923bcf8fbbb2388f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:19:39 -0500 Subject: [PATCH 034/149] Delete changelog files based on PR 35207 (#35228) Delete changelog files for 35207 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-warning_order | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-warning_order diff --git a/plugins/woocommerce/changelog/fix-warning_order b/plugins/woocommerce/changelog/fix-warning_order deleted file mode 100644 index f0f37f3dce1..00000000000 --- a/plugins/woocommerce/changelog/fix-warning_order +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Check whether order has classname before returning. From c50f69e22960dd450a5191a91f3ded2c3fd5fe3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:23:11 -0500 Subject: [PATCH 035/149] Delete changelog files based on PR 35087 (#35230) Delete changelog files for 35087 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-35018-untrash-order-hook | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35018-untrash-order-hook diff --git a/plugins/woocommerce/changelog/fix-35018-untrash-order-hook b/plugins/woocommerce/changelog/fix-35018-untrash-order-hook deleted file mode 100644 index 65546d1d723..00000000000 --- a/plugins/woocommerce/changelog/fix-35018-untrash-order-hook +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Introduces new hook `woocommerce_untrash_order` as a COT/HPOS analog to `untrash_post`. From d0278f4801f0edc72310796687c8539d805e3cde Mon Sep 17 00:00:00 2001 From: Thomas Roberts <5656702+opr@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:34:59 +0100 Subject: [PATCH 036/149] Update WooCommerce Blocks package to 8.7.3 (#35219) * Update WooCommerce Blocks package to 8.7.3 * Updated composer.lock Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com> --- .../changelog/update-woocommerce-blocks-8.7.3 | 4 ++++ plugins/woocommerce/composer.json | 2 +- plugins/woocommerce/composer.lock | 14 +++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 new file mode 100644 index 00000000000..640eae41f1a --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Comment: Update WooCommerce Blocks to 8.7.3 diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 71dd3e11190..aabb8e10944 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.4.2", - "woocommerce/woocommerce-blocks": "8.7.2" + "woocommerce/woocommerce-blocks": "8.7.3" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 2bd7f28345a..308d3636793 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -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": "d1c35e053ae34c5434635a435e326266", + "content-hash": "01f8810be9f7fc1f6404f4e02a59f10c", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v8.7.2", + "version": "v8.7.3", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "2ff8573d5426ebee180e50539f113c211362d1b9" + "reference": "6c3c5d5c7312bcd6c6fac1d9a96ae87141b52ae1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/2ff8573d5426ebee180e50539f113c211362d1b9", - "reference": "2ff8573d5426ebee180e50539f113c211362d1b9", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/6c3c5d5c7312bcd6c6fac1d9a96ae87141b52ae1", + "reference": "6c3c5d5c7312bcd6c6fac1d9a96ae87141b52ae1", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.2" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.3" }, - "time": "2022-10-14T12:13:45+00:00" + "time": "2022-10-20T17:09:09+00:00" } ], "packages-dev": [ From 114be56f630aa01a4bbbb338863ec17359e86383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Thu, 20 Oct 2022 17:15:44 -0300 Subject: [PATCH 037/149] Move product action buttons to header menu (#35214) * Move product action buttons to header menu * Fix unit tests * Add comment suggestions --- .../client/activity-panel/activity-panel.js | 15 +- .../client/products/product-form-actions.scss | 1 + .../client/products/product-form-actions.tsx | 251 ++++++++++-------- .../test/product-form-actions.spec.tsx | 8 + .../client/utils/test/url-helpers.spec.ts | 25 ++ .../client/utils/url-helpers.ts | 11 + .../changelog/add-35139-header-action-button | 4 + 7 files changed, 198 insertions(+), 117 deletions(-) create mode 100644 plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts create mode 100644 plugins/woocommerce-admin/client/utils/url-helpers.ts create mode 100644 plugins/woocommerce/changelog/add-35139-header-action-button diff --git a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js index 0a5690591fc..d94961584fd 100644 --- a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js +++ b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js @@ -45,6 +45,7 @@ import { ABBREVIATED_NOTIFICATION_SLOT_NAME } from './panels/inbox/abbreviated-n import { getAdminSetting } from '~/utils/admin-settings'; import { useActiveSetupTasklist } from '~/tasks'; import { LayoutContext } from '~/layout'; +import { getSegmentsFromPath } from '~/utils/url-helpers'; const HelpPanel = lazy( () => import( /* webpackChunkName: "activity-panels-help" */ './panels/help' ) @@ -236,6 +237,13 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { return query.page === 'wc-admin' && ! query.path; }; + const isProductPage = () => { + const [ firstPathSegment ] = getSegmentsFromPath( query.path ); + return ( + firstPathSegment === 'add-product' || firstPathSegment === 'product' + ); + }; + const isPerformingSetupTask = () => { return ( query.task && @@ -254,7 +262,9 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { icon: , unread: hasUnreadNotes || hasAbbreviatedNotifications, visible: - ( isEmbedded || ! isHomescreen() ) && ! isPerformingSetupTask(), + ( isEmbedded || ! isHomescreen() ) && + ! isPerformingSetupTask() && + ! isProductPage(), }; const setup = { @@ -273,7 +283,8 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { ! requestingTaskListOptions && ! setupTaskListComplete && ! setupTaskListHidden && - ! isHomescreen(), + ! isHomescreen() && + ! isProductPage(), }; const help = { diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.scss b/plugins/woocommerce-admin/client/products/product-form-actions.scss index 724363ad170..47d6ee040e7 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.scss +++ b/plugins/woocommerce-admin/client/products/product-form-actions.scss @@ -6,6 +6,7 @@ $gutenberg-blue-darker: var(--wp-admin-theme-color-darker-20); flex-direction: row; align-items: center; justify-content: flex-end; + padding-right: var(--large-gap); > .components-button { margin-right: $gap-smaller; diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.tsx b/plugins/woocommerce-admin/client/products/product-form-actions.tsx index 1e53fdab129..ac0507aee39 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.tsx +++ b/plugins/woocommerce-admin/client/products/product-form-actions.tsx @@ -10,6 +10,7 @@ import { MenuItem, } from '@wordpress/components'; import { chevronDown, check, Icon } from '@wordpress/icons'; +import { registerPlugin } from '@wordpress/plugins'; import { useFormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; @@ -17,8 +18,9 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import './product-form-actions.scss'; +import { WooHeaderItem } from '~/header/utils'; import { useProductHelper } from './use-product-helper'; +import './product-form-actions.scss'; export const ProductFormActions: React.FC = () => { const { @@ -122,120 +124,139 @@ export const ProductFormActions: React.FC = () => { const isPublished = values.id && values.status === 'publish'; return ( -
- - - - - - { () => ( - <> - - - { isPublished - ? __( - 'Update & duplicate', + + { () => ( +
+ + + + + + { () => ( + <> + + + { isPublished + ? __( + 'Update & duplicate', + 'woocommerce' + ) + : __( + 'Publish & duplicate', + 'woocommerce' + ) } + + + { __( + 'Copy to a new draft', 'woocommerce' - ) - : __( - 'Publish & duplicate', - 'woocommerce' - ) } - - - { __( - 'Copy to a new draft', - 'woocommerce' - ) } - - { values.id && ( - - { __( 'Move to trash', 'woocommerce' ) } - - ) } - - - ) } - - -
+ ) } +
+ { values.id && ( + + { __( + 'Move to trash', + 'woocommerce' + ) } + + ) } +
+ + ) } +
+
+
+ ) } + ); }; + +registerPlugin( 'action-buttons-header-item', { + render: ProductFormActions, + icon: 'admin-generic', +} ); diff --git a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx index e5f9789fd9d..565189510a0 100644 --- a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx +++ b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx @@ -1,7 +1,9 @@ /** * External dependencies */ +import { PropsWithChildren } from 'react'; import { render, waitFor, screen, within } from '@testing-library/react'; +import { Fragment } from '@wordpress/element'; import { Form, FormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; @@ -18,7 +20,13 @@ const updateProductWithStatus = jest.fn(); const copyProductWithStatus = jest.fn(); const deleteProductAndRedirect = jest.fn(); +jest.mock( '@wordpress/plugins', () => ( { registerPlugin: jest.fn() } ) ); jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); +jest.mock( '~/header/utils', () => ( { + WooHeaderItem: ( props: { children: () => React.ReactElement } ) => ( + { props.children() } + ), +} ) ); jest.mock( '../use-product-helper', () => { return { useProductHelper: () => ( { diff --git a/plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts b/plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts new file mode 100644 index 00000000000..de094bc823e --- /dev/null +++ b/plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ +import { getSegmentsFromPath } from '../url-helpers'; + +describe( 'URL Helpers', () => { + it( 'should extract segments from query path param correctly', () => { + const paths = [ + { input: undefined, output: [] }, + { input: '', output: [ '' ] }, + { input: 'product', output: [ 'product' ] }, + { input: 'product/', output: [ 'product' ] }, + { input: '/product', output: [ 'product' ] }, + { input: '/product/', output: [ 'product' ] }, + { input: 'product/123', output: [ 'product', '123' ] }, + { input: 'product/123/', output: [ 'product', '123' ] }, + { input: '/product/123', output: [ 'product', '123' ] }, + { input: '/product/123/', output: [ 'product', '123' ] }, + ]; + + paths.forEach( ( { input, output } ) => { + expect( getSegmentsFromPath( input ) ).toStrictEqual( output ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/utils/url-helpers.ts b/plugins/woocommerce-admin/client/utils/url-helpers.ts new file mode 100644 index 00000000000..2a4de62a627 --- /dev/null +++ b/plugins/woocommerce-admin/client/utils/url-helpers.ts @@ -0,0 +1,11 @@ +/** + * Extracts all segments from the path query param as a string array. + * + * @param path The query path param + * @return The list of segments from the path + */ +export function getSegmentsFromPath( path?: string ): string[] { + const firstIndex = path?.startsWith( '/' ) ? 1 : 0; + const lastIndex = path?.endsWith( '/' ) ? -1 : undefined; + return path?.slice( firstIndex, lastIndex )?.split( '/' ) ?? []; +} diff --git a/plugins/woocommerce/changelog/add-35139-header-action-button b/plugins/woocommerce/changelog/add-35139-header-action-button new file mode 100644 index 00000000000..b7055d8a883 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35139-header-action-button @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Move product action buttons to header menu From 3a2a22905ba12df5ee05ba244ef1a7d042e543d0 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 20 Oct 2022 14:14:54 -0700 Subject: [PATCH 038/149] Add manual stock management section to product management experience (#35047) * Add manual stock management section to product management experience * Add changelog entry * Add default value for stock status * Fix up lint issues * Handle PR feedback --- .../client/products/add-product-page.tsx | 2 +- .../products/layout/product-form-layout.scss | 4 +- .../layout/product-section-layout.scss | 6 ++- .../manual-stock-section.tsx | 39 +++++++++++++++++++ .../product-inventory-section.tsx | 2 + plugins/woocommerce/changelog/add-34388 | 4 ++ 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx create mode 100644 plugins/woocommerce/changelog/add-34388 diff --git a/plugins/woocommerce-admin/client/products/add-product-page.tsx b/plugins/woocommerce-admin/client/products/add-product-page.tsx index e594bbf2bfd..353de8e3165 100644 --- a/plugins/woocommerce-admin/client/products/add-product-page.tsx +++ b/plugins/woocommerce-admin/client/products/add-product-page.tsx @@ -28,7 +28,7 @@ const AddProductPage: React.FC = () => { return (
> - initialValues={ { stock_quantity: 0 } } + initialValues={ { stock_quantity: 0, stock_status: 'instock' } } errors={ {} } validate={ validate } > diff --git a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss index 2e49589b949..06772f0b89b 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss @@ -2,11 +2,13 @@ max-width: 1032px; margin: 0 auto; - h4 { + h4, + .components-radio-control .components-base-control__label { font-size: 14px; font-weight: 600; margin-bottom: 18px; margin-top: 26px; + text-transform: none; } .components-card__body h4:first-child { diff --git a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss index 82fa017706b..3558a817a3c 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss @@ -15,11 +15,15 @@ .components-base-control, .woocommerce-rich-text-editor { - &:not(:first-child) { + &:not(:first-child):not(.components-radio-control) { margin-top: $gap-large - $gap-smaller; margin-bottom: 0; } } + + .components-radio-control .components-v-stack { + gap: $gap-small; + } } &__header { diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx new file mode 100644 index 00000000000..f815f8ef5c3 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { RadioControl } from '@wordpress/components'; +import { useFormContext } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; + +export const ManualStockSection: React.FC = () => { + const { getInputProps } = useFormContext< Product >(); + const inputProps = getInputProps( 'stock_status' ); + // These properties cause issues with the RadioControl component. + // A fix to form upstream would help if we can identify what type of input is used. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete inputProps.checked; + delete inputProps.value; + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx index 1ab01f095c2..f8950dcbc38 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx @@ -20,6 +20,7 @@ import { getCheckboxProps, getTextControlProps } from '../utils'; import { getAdminSetting } from '~/utils/admin-settings'; import { ProductSectionLayout } from '../../layout/product-section-layout'; import { ManageStockSection } from './manage-stock-section'; +import { ManualStockSection } from './manual-stock-section'; export const ProductInventorySection: React.FC = () => { const { getInputProps, values } = useFormContext< Product >(); @@ -82,6 +83,7 @@ export const ProductInventorySection: React.FC = () => { { values.manage_stock && } ) } + { ! values.manage_stock && } diff --git a/plugins/woocommerce/changelog/add-34388 b/plugins/woocommerce/changelog/add-34388 new file mode 100644 index 00000000000..7330ad35258 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34388 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add manual stock management section to product management experience From 976e2f9e0bb98d494e0de555a793e95608084c65 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Fri, 21 Oct 2022 15:14:10 +1300 Subject: [PATCH 039/149] Declare npm registry when setting up node to allow NPM auth to work (#35235) Declare npm registry when setting up node to allow NPM auth to work --- .github/actions/setup-woocommerce-monorepo/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index a97ef63b1cd..b83f9809fb0 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -35,6 +35,7 @@ runs: with: node-version-file: .nvmrc cache: pnpm + registry-url: 'https://registry.npmjs.org' - name: Setup PHP uses: shivammathur/setup-php@e04e1d97f0c0481c6e1ba40f8a538454fe5d7709 From c20f58e16a5408c59049ea150ba7eff209a4c3f5 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 21 Oct 2022 08:56:46 +0200 Subject: [PATCH 040/149] Fix 'Invalid payment method' error upon double click 'Delete' #30862 (#30884) * Fix 'Invalid payment method' error upon double click 'Delete' #30862 An 'Invalid payment method' error shows when a user double clicks the 'Delete' button in the 'Add Payment method screen'. Every time the user clicks the button, there is a GET request to delete the payment method. First request is handled correctly, but subsequent requests throw an error, because the payment method cannot be found since it was already deleted. This adds an event handler to disable the button on the first click. * Add changelog entry --- .../fix-invalid-payment-error-upon-double-click-delete | 4 ++++ .../client/legacy/js/frontend/add-payment-method.js | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete diff --git a/plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete b/plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete new file mode 100644 index 00000000000..64a2fd56147 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix 'Invalid payment method' error upon double click on Delete button of Payment methods table diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js b/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js index b200c6921fe..1ccdefac3cb 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js @@ -32,4 +32,13 @@ jQuery( function( $ ) { $( document.body ).trigger( 'init_add_payment_method' ); + // Prevent firing multiple requests upon double clicking the buttons in payment methods table + $(' .woocommerce .payment-method-actions .button.delete' ).on( 'click' , function( event ) { + if ( $( this ).hasClass( 'disabled' ) ) { + event.preventDefault(); + } + + $( this ).addClass( 'disabled' ); + }); + }); From 97773025c0310bf1f155c942a51ae2cfde6dd353 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 21 Oct 2022 11:31:01 +0200 Subject: [PATCH 041/149] Improve unit test install script for db connection over socket (#35152) --- plugins/woocommerce/changelog/fix-test-install-script | 4 ++++ plugins/woocommerce/tests/bin/install.sh | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-test-install-script diff --git a/plugins/woocommerce/changelog/fix-test-install-script b/plugins/woocommerce/changelog/fix-test-install-script new file mode 100644 index 00000000000..f6182141871 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-test-install-script @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update unit test install script for db sockets. diff --git a/plugins/woocommerce/tests/bin/install.sh b/plugins/woocommerce/tests/bin/install.sh index 7bcc72190ea..05f7e9065ad 100755 --- a/plugins/woocommerce/tests/bin/install.sh +++ b/plugins/woocommerce/tests/bin/install.sh @@ -131,7 +131,11 @@ install_test_suite() { sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + if [[ "$DB_HOST" == *.sock ]]; then + sed $ioption "s|localhost|:${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + else + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi fi } @@ -145,7 +149,7 @@ install_db() { # If we're trying to connect to a socket we want to handle it differently. if [[ "$DB_HOST" == *.sock ]]; then # create database using the socket - mysqladmin create $DB_NAME --socket="$DB_HOST" + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS" --socket="$DB_HOST" else # Decide whether or not there is a port. local PARTS=(${DB_HOST//\:/ }) From fa20d434f325670e8bfea41d51e63fdcd8d89428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Fri, 21 Oct 2022 11:48:54 +0200 Subject: [PATCH 042/149] [COT/HPOS] Fix trashing/untrashing of orders (#35125) * Fix trashing/untrashing orders when using COT: - Trashing wasn't changing the update date on the orders table record. - Trashing wasn't applying trash on the corresponding post. - Untrashing wasn't actually deleting _wp_trash_meta_status in metadata. * Fix: maybe_sync_order was syncing even with identical update dates. * PHPCS is happy now. * WP_DIE when attempting to load a trashed order in Admin (same behavior as when using the posts table) * Add changelog file * Revert change in order records date comparison Data comparison is needed too when the dates are equal, since data in the metadata table could have been modified as well. Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Fix handling of order status and trash meta when untrashing. Untrashing an order was setting it to the default status of "pending" instead of the previous status. This is what was happening: 1. Order is trashed, order meta table gets the value (e.g.) "on-hold" for the _wp_trash_meta_status key 2. Post is trashed, post meta table gets "wc-on-hold" 3. Order is re-read, but then so is posts, and the order meta value is updated to "wc-on-hold" 4. On untrash the previous order status from order meta table is prefixed with "wc-", thus it becomes "wc-wc-on-hold", which is not a valid status 5. Not having a valid status, the order is restored as the default status of "pending payment" To fix this, the previous status is stored in order meta table with "wc-", and it doesn't get prefixed on untrash. Also, wp_untrash_post is called on order untrash, otherwise the trash metadata keeps dangling and being synced back and forth. * Add unit tetst for the trash-untrash cycle Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/changelog/fix-35089 | 4 ++ .../Internal/Admin/Orders/PageController.php | 3 ++ .../Orders/OrdersTableDataStore.php | 39 +++++++++++++++---- .../Orders/OrdersTableDataStoreTests.php | 32 +++++++++++++++ 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-35089 diff --git a/plugins/woocommerce/changelog/fix-35089 b/plugins/woocommerce/changelog/fix-35089 new file mode 100644 index 00000000000..69818871691 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35089 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix (un)trashing of orders when using HPOS diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php index 01b48ec8eaf..695fbeef930 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php @@ -58,6 +58,9 @@ class PageController { if ( ! current_user_can( 'edit_others_shop_orders' ) && ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit this order', 'woocommerce' ) ); } + if ( 'trash' === $this->order->get_status() ) { + wp_die( esc_html__( 'You cannot edit this item because it is in the Trash. Please restore it and try again.', 'woocommerce' ) ); + } } /** diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 7d94b7025b2..92229dfd2c0 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1059,9 +1059,7 @@ WHERE * * So we write back to the order table when order modified date is more recent than post modified date. Otherwise, we write to the post table. */ - if ( $order_modified_date > $post_order_modified_date ) { - return; - } else { + if ( $post_order_modified_date >= $order_modified_date ) { $this->migrate_post_record( $order, $post_order ); } } @@ -1759,7 +1757,7 @@ FROM $order_meta_table } $trash_metadata = array( - '_wp_trash_meta_status' => $order->get_status( 'edit' ), + '_wp_trash_meta_status' => 'wc-' . $order->get_status( 'edit' ), '_wp_trash_meta_time' => time(), ); @@ -1775,13 +1773,21 @@ FROM $order_meta_table $wpdb->update( self::get_orders_table_name(), - array( 'status' => 'trash' ), + array( + 'status' => 'trash', + 'date_updated_gmt' => current_time( 'Y-m-d H:i:s', true ), + ), array( 'id' => $order->get_id() ), - array( '%s' ), + array( '%s', '%s' ), array( '%d' ) ); $order->set_status( 'trash' ); + + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() ) { + wp_trash_post( $order->get_id() ); + } } /** @@ -1809,7 +1815,7 @@ FROM $order_meta_table $previous_status = $order->get_meta( '_wp_trash_meta_status' ); $valid_statuses = wc_get_order_statuses(); - $previous_state_is_invalid = ! array_key_exists( 'wc-' . $previous_status, $valid_statuses ); + $previous_state_is_invalid = ! array_key_exists( $previous_status, $valid_statuses ); $pending_is_valid_status = array_key_exists( 'wc-pending', $valid_statuses ); if ( $previous_state_is_invalid && $pending_is_valid_status ) { @@ -1852,9 +1858,26 @@ FROM $order_meta_table $order->save(); // Was the status successfully restored? Let's clean up the meta and indicate success... - if ( $previous_status === $order->get_status() ) { + if ( 'wc-' . $order->get_status() === $previous_status ) { $order->delete_meta_data( '_wp_trash_meta_status' ); $order->delete_meta_data( '_wp_trash_meta_time' ); + $order->delete_meta_data( '_wp_trash_meta_comments_status' ); + $order->save_meta_data(); + + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() ) { + //The previous $order->save() will have forced a sync to the posts table, + //this implies that the post status is not "trash" anymore, and thus + //wp_untrash_post would do nothing. + wp_update_post( + array( + 'ID' => $id, + 'post_status' => 'trash', + ) + ); + + wp_untrash_post( $id ); + } return true; } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 5e38ff264cb..4c838c25eab 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -480,6 +480,38 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { } } + /** + * @testdox Test the trash-untrash cycle with sync enabled. + */ + public function test_cot_datastore_untrash() { + global $wpdb; + + $this->enable_cot_sync(); + + // Tests trashing of orders. + $order = $this->create_complex_cot_order(); + $order->set_status( 'on-hold' ); + $order->save(); + $order_id = $order->get_id(); + + $this->sut->trash_order( $order ); + + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders + $orders_table = $this->sut::get_orders_table_name(); + $this->assertEquals( 'trash', $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$orders_table} WHERE id = %d", $order_id ) ) ); + $this->assertEquals( 'trash', $wpdb->get_var( $wpdb->prepare( "SELECT post_status FROM {$wpdb->posts} WHERE id = %d", $order_id ) ) ); + + $this->sut->read( $order ); + $this->sut->untrash_order( $order ); + + $this->assertEquals( 'on-hold', $order->get_status() ); + $this->assertEquals( 'wc-on-hold', get_post_status( $order_id ) ); + + $this->assertEmpty( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->sut->get_meta_table_name()} WHERE order_id = %d AND meta_key LIKE '_wp_trash_meta_%'", $order_id ) ) ); + $this->assertEmpty( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE '_wp_trash_meta_%'", $order_id ) ) ); + //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders + } + /** * @testDox Tests the `delete()` method on the COT datastore -- full deletes. * From be6c26b671f1cdebc4da15d0af0a990e42e8bb95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Oct 2022 08:42:49 -0500 Subject: [PATCH 043/149] Delete changelog files based on PR 35125 (#35246) Delete changelog files for 35125 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-35089 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35089 diff --git a/plugins/woocommerce/changelog/fix-35089 b/plugins/woocommerce/changelog/fix-35089 deleted file mode 100644 index 69818871691..00000000000 --- a/plugins/woocommerce/changelog/fix-35089 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix (un)trashing of orders when using HPOS From 28f8e7f996f85f2814d332c50988c8b59daf8570 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Fri, 21 Oct 2022 07:03:49 -0700 Subject: [PATCH 044/149] Enhance getInputProps to allow passing of non-overridden props (#35034) * Allow additional props to be passed to the Form getInputProps method * Remove getTextControlProps * Pass additional shared props through getInputProps in shipping * Simplify checkbox props * Unwrap currency props * Use onBlur event to sanitize prices * Add changelog entry * Add option to get checkbox props to form context helpers * Update checkbox tracks handler naming and typing * Fix up usage of getInputProps * Add helper sanitize method * Use sanitize helper method for product input fields * Fix inventory input props after rebase * Fix shipping typo * Fix up form types after rebase * Align all checkboxes on product page * Rename new checkbox helper to getCheckboxControlProps * Add helper method to get select control props * Add data changelog entry * Check for product name length on blur * Add initial value for name to prevent uncontrolled value * Add initial value for sku --- packages/js/components/changelog/update-34996 | 4 + .../js/components/src/form/form-context.ts | 34 +++-- packages/js/components/src/form/form.tsx | 101 +++++++++++++- packages/js/data/changelog/update-34996 | 4 + packages/js/data/src/products/types.ts | 1 + .../client/products/add-product-page.tsx | 7 +- .../client/products/product-page.scss | 17 ++- .../products/sections/pricing-section.tsx | 41 +++--- .../sections/product-details-section.scss | 15 -- .../sections/product-details-section.tsx | 34 ++--- .../manage-stock-section.tsx | 9 +- .../product-inventory-section.tsx | 12 +- .../sections/product-shipping-section.tsx | 101 ++++++-------- .../client/products/sections/utils.ts | 129 +++++++----------- .../add-new-shipping-class-modal.tsx | 17 +-- plugins/woocommerce/changelog/update-34996 | 4 + 16 files changed, 292 insertions(+), 238 deletions(-) create mode 100644 packages/js/components/changelog/update-34996 create mode 100644 packages/js/data/changelog/update-34996 create mode 100644 plugins/woocommerce/changelog/update-34996 diff --git a/packages/js/components/changelog/update-34996 b/packages/js/components/changelog/update-34996 new file mode 100644 index 00000000000..5b25d4c8856 --- /dev/null +++ b/packages/js/components/changelog/update-34996 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Allow passing of additional props to form inputs diff --git a/packages/js/components/src/form/form-context.ts b/packages/js/components/src/form/form-context.ts index 5fcaad42ee4..be43a98a9d6 100644 --- a/packages/js/components/src/form/form-context.ts +++ b/packages/js/components/src/form/form-context.ts @@ -1,23 +1,22 @@ /** * External dependencies */ -import { ChangeEvent } from 'react'; import { createContext, useContext } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { + CheckboxProps, + ConsumerInputProps, + InputProps, + SelectControlProps, +} from './form'; + export type FormErrors< Values > = { [ P in keyof Values ]?: FormErrors< Values[ P ] > | string; }; -export type InputProps< Value > = { - value: Value; - checked: boolean; - selected?: boolean; - onChange: ( value: ChangeEvent< HTMLInputElement > | Value ) => void; - onBlur: () => void; - className: string | undefined; - help: string | null | undefined; -}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FormContext< Values extends Record< string, any > > = { values: Values; @@ -31,9 +30,18 @@ export type FormContext< Values extends Record< string, any > > = { setValue: ( name: string, value: any ) => void; setValues: ( valuesToSet: Values ) => void; handleSubmit: () => Promise< Values >; + getCheckboxControlProps< Value extends Values[ keyof Values ] >( + name: string, + inputProps?: ConsumerInputProps< Values > + ): CheckboxProps< Values, Value >; + getSelectControlProps< Value extends Values[ keyof Values ] >( + name: string, + inputProps?: ConsumerInputProps< Values > + ): SelectControlProps< Values, Value >; getInputProps< Value extends Values[ keyof Values ] >( - name: string - ): InputProps< Value >; + name: string, + inputProps?: ConsumerInputProps< Values > + ): InputProps< Values, Value >; isValidForm: boolean; resetForm: ( initialValues: Values, diff --git a/packages/js/components/src/form/form.tsx b/packages/js/components/src/form/form.tsx index 547fa49ab4d..878c6ce6ef0 100644 --- a/packages/js/components/src/form/form.tsx +++ b/packages/js/components/src/form/form.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import classnames from 'classnames'; import { cloneElement, useState, @@ -17,11 +18,12 @@ import _setWith from 'lodash/setWith'; import _get from 'lodash/get'; import _clone from 'lodash/clone'; import _isEqual from 'lodash/isEqual'; +import _omit from 'lodash/omit'; /** * Internal dependencies */ -import { FormContext, FormErrors, InputProps } from './form-context'; +import { FormContext, FormErrors } from './form-context'; type FormProps< Values > = { /** @@ -87,6 +89,40 @@ export type FormRef< Values > = { resetForm: ( initialValues: Values ) => void; }; +export type InputProps< Values, Value > = { + value: Value; + checked: boolean; + selected?: boolean; + onChange: ( + value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] + ) => void; + onBlur: () => void; + className: string | undefined; + help: string | null | undefined; +}; + +export type CheckboxProps< Values, Value > = Omit< + InputProps< Values, Value >, + 'value' | 'selected' +>; + +export type SelectControlProps< Values, Value > = Omit< + InputProps< Values, Value >, + 'value' +> & { + value: string | undefined; +}; + +export type ConsumerInputProps< Values > = { + className?: string; + onChange?: ( + value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] + ) => void; + onBlur?: () => void; + [ key: string ]: unknown; + sanitize?: ( value: Values[ keyof Values ] ) => Values[ keyof Values ]; +}; + /** * A form component to handle form state and provide input helper props. */ @@ -268,21 +304,70 @@ function FormComponent< Values extends Record< string, any > >( }; function getInputProps< Value = Values[ keyof Values ] >( - name: string - ): InputProps< Value > { + name: string, + inputProps: ConsumerInputProps< Values > = {} + ): InputProps< Values, Value > { const inputValue = _get( values, name ); const isTouched = touched[ name ]; const inputError = _get( errors, name ); + const { + className: classNameProp, + onBlur: onBlurProp, + onChange: onChangeProp, + sanitize, + ...additionalProps + } = inputProps; return { value: inputValue, checked: Boolean( inputValue ), selected: inputValue, - onChange: ( value: ChangeEvent< HTMLInputElement > | Value ) => - handleChange( name, value as Values[ keyof Values ] ), - onBlur: () => handleBlur( name ), - className: isTouched && inputError ? 'has-error' : undefined, + onChange: ( + value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] + ) => { + handleChange( name, value ); + if ( onChangeProp ) { + onChangeProp( value ); + } + }, + onBlur: () => { + if ( sanitize ) { + handleChange( name, sanitize( inputValue ) ); + } + handleBlur( name ); + if ( onBlurProp ) { + onBlurProp(); + } + }, + className: classnames( classNameProp, { + 'has-error': isTouched && inputError, + } ), help: isTouched ? ( inputError as string ) : null, + ...additionalProps, + }; + } + + function getCheckboxControlProps< Value = Values[ keyof Values ] >( + name: string, + inputProps: ConsumerInputProps< Values > = {} + ): CheckboxProps< Values, Value > { + return _omit( getInputProps( name, inputProps ), [ + 'selected', + 'value', + ] ); + } + + function getSelectControlProps< Value = Values[ keyof Values ] >( + name: string, + inputProps: ConsumerInputProps< Values > = {} + ): SelectControlProps< Values, Value > { + const selectControlProps = getInputProps( name, inputProps ); + return { + ...selectControlProps, + value: + selectControlProps.value === undefined + ? undefined + : String( selectControlProps.value ), }; } @@ -301,7 +386,9 @@ function FormComponent< Values extends Record< string, any > >( setValue, setValues, handleSubmit, + getCheckboxControlProps, getInputProps, + getSelectControlProps, isValidForm: ! Object.keys( errors ).length, resetForm, }; diff --git a/packages/js/data/changelog/update-34996 b/packages/js/data/changelog/update-34996 new file mode 100644 index 00000000000..f7695b9e16c --- /dev/null +++ b/packages/js/data/changelog/update-34996 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Add missing shipping class property diff --git a/packages/js/data/src/products/types.ts b/packages/js/data/src/products/types.ts index 57da249e4b1..961261a771f 100644 --- a/packages/js/data/src/products/types.ts +++ b/packages/js/data/src/products/types.ts @@ -86,6 +86,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit< backordered: boolean; shipping_required: boolean; shipping_taxable: boolean; + shipping_class: string; shipping_class_id: number; average_rating: string; rating_count: number; diff --git a/plugins/woocommerce-admin/client/products/add-product-page.tsx b/plugins/woocommerce-admin/client/products/add-product-page.tsx index 353de8e3165..5c2ed73dd1e 100644 --- a/plugins/woocommerce-admin/client/products/add-product-page.tsx +++ b/plugins/woocommerce-admin/client/products/add-product-page.tsx @@ -28,7 +28,12 @@ const AddProductPage: React.FC = () => { return (
> - initialValues={ { stock_quantity: 0, stock_status: 'instock' } } + initialValues={ { + name: '', + sku: '', + stock_quantity: 0, + stock_status: 'instock', + } } errors={ {} } validate={ validate } > diff --git a/plugins/woocommerce-admin/client/products/product-page.scss b/plugins/woocommerce-admin/client/products/product-page.scss index d7125bc6f76..23c9c9ac2d8 100644 --- a/plugins/woocommerce-admin/client/products/product-page.scss +++ b/plugins/woocommerce-admin/client/products/product-page.scss @@ -3,7 +3,7 @@ .woocommerce-product-form-actions { margin-top: $gap-largest + $gap-smaller; } - .woocommerce-product__checkbox, + .components-checkbox-control, .components-toggle-control { & > * { margin-bottom: 0; @@ -17,8 +17,19 @@ margin-right: $gap-smaller; } } - .components-checkbox-control__label { - align-items: center; + .components-checkbox-control { + &__label { + display: flex; + align-items: center; + } + + &__input-container { + align-self: center; + } + + .components-base-control__field { + display: flex; + } } .woocommerce-tooltip { margin-left: $gap-smaller; diff --git a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx index 956efb3e7b2..6dae5ad1310 100644 --- a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx @@ -20,8 +20,8 @@ import { * Internal dependencies */ import './pricing-section.scss'; +import { formatCurrencyDisplayValue, getCurrencySymbolProps } from './utils'; import { ProductSectionLayout } from '../layout/product-section-layout'; -import { getInputControlProps } from './utils'; import { ADMIN_URL } from '../../utils/admin-settings'; import { CurrencyContext } from '../../lib/currency-context'; import { useProductHelper } from '../use-product-helper'; @@ -44,6 +44,8 @@ export const PricingSection: React.FC = () => { const pricesIncludeTax = taxSettings.woocommerce_prices_include_tax === 'yes'; const context = useContext( CurrencyContext ); + const { getCurrencyConfig, formatAmount } = context; + const currencyConfig = getCurrencyConfig(); const taxIncludedInPriceText = __( 'Per your {{link}}store settings{{/link}}, tax is {{strong}}included{{/strong}} in the price.', @@ -87,14 +89,17 @@ export const PricingSection: React.FC = () => { }, } ); - const regularPriceProps = getInputControlProps( { - ...getInputProps( 'regular_price' ), - context, - } ); - const salePriceProps = getInputControlProps( { - ...getInputProps( 'sale_price' ), - context, - } ); + const currencyInputProps = { + ...getCurrencySymbolProps( currencyConfig ), + sanitize: ( value: Product[ keyof Product ] ) => { + return sanitizePrice( String( value ) ); + }, + }; + const regularPriceProps = getInputProps( + 'regular_price', + currencyInputProps + ); + const salePriceProps = getInputProps( 'sale_price', currencyInputProps ); return ( { { ...regularPriceProps } label={ __( 'List price', 'woocommerce' ) } placeholder={ __( '10.59', 'woocommerce' ) } - onChange={ ( value: string ) => { - const sanitizedValue = sanitizePrice( value ); - regularPriceProps?.onChange( sanitizedValue ); - } } + value={ formatCurrencyDisplayValue( + String( regularPriceProps?.value ), + currencyConfig, + formatAmount + ) } /> { ! isTaxSettingsResolving && ( @@ -156,10 +162,11 @@ export const PricingSection: React.FC = () => { { ...salePriceProps } label={ salePriceTitle } placeholder={ __( '8.59', 'woocommerce' ) } - onChange={ ( value: string ) => { - const sanitizedValue = sanitizePrice( value ); - salePriceProps?.onChange( sanitizedValue ); - } } + value={ formatCurrencyDisplayValue( + String( salePriceProps?.value ), + currencyConfig, + formatAmount + ) } /> diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss b/plugins/woocommerce-admin/client/products/sections/product-details-section.scss index c6f7b22be65..d5f37a1907f 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.scss @@ -12,19 +12,4 @@ margin-left: $gap-smaller; } } - - &__feature-checkbox { - .components-base-control__field { - display: flex; - .components-checkbox-control { - &__label { - display: flex; - } - - &__input-container { - align-self: center; - } - } - } - } } diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index 910bc0d921f..c7cb7517f36 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -34,14 +34,20 @@ import { BlockInstance, serialize, parse } from '@wordpress/blocks'; import './product-details-section.scss'; import { CategoryField } from '../fields/category-field'; import { EditProductLinkModal } from '../shared/edit-product-link-modal'; -import { getCheckboxProps, getTextControlProps } from './utils'; +import { getCheckboxTracks } from './utils'; import { ProductSectionLayout } from '../layout/product-section-layout'; const PRODUCT_DETAILS_SLUG = 'product-details'; export const ProductDetailsSection: React.FC = () => { - const { getInputProps, values, setValue, touched, errors } = - useFormContext< Product >(); + const { + getCheckboxControlProps, + getInputProps, + values, + touched, + errors, + setValue, + } = useFormContext< Product >(); const [ showProductLinkEditModal, setShowProductLinkEditModal ] = useState( false ); const [ descriptionBlocks, setDescriptionBlocks ] = useState< @@ -66,7 +72,7 @@ export const ProductDetailsSection: React.FC = () => { }; const setSkuIfEmpty = () => { - if ( values.sku || ! values.name.length ) { + if ( values.sku || ! values.name?.length ) { return; } setValue( 'sku', cleanForSlug( values.name ) ); @@ -90,13 +96,9 @@ export const ProductDetailsSection: React.FC = () => { 'e.g. 12 oz Coffee Mug', 'woocommerce' ) } - { ...getTextControlProps( - getInputProps( 'name' ) - ) } - onBlur={ () => { - setSkuIfEmpty(); - getInputProps( 'name' ).onBlur(); - } } + { ...getInputProps( 'name', { + onBlur: setSkuIfEmpty, + } ) } /> { values.id && ! hasNameError() && permalinkPrefix && ( @@ -170,12 +172,10 @@ export const ProductDetailsSection: React.FC = () => { /> } - { ...getCheckboxProps( { - ...getInputProps( 'featured' ), - name: 'featured', - className: - 'product-details-section__feature-checkbox', - } ) } + { ...getCheckboxControlProps( + 'featured', + getCheckboxTracks( 'featured' ) + ) } /> { showProductLinkEditModal && ( { const { getInputProps } = useFormContext< Product >(); @@ -25,9 +24,7 @@ export const ManageStockSection: React.FC = () => { { __( '%d (store default)', 'woocommerce' ), notifyLowStockAmount ) } - { ...getTextControlProps( { - ...getInputProps( 'low_stock_amount' ), - } ) } + { ...getInputProps( 'low_stock_amount' ) } min={ 0 } /> diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx index f8950dcbc38..cc565a19eb7 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx @@ -16,14 +16,15 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import { getCheckboxProps, getTextControlProps } from '../utils'; +import { getCheckboxTracks } from '../utils'; import { getAdminSetting } from '~/utils/admin-settings'; import { ProductSectionLayout } from '../../layout/product-section-layout'; import { ManageStockSection } from './manage-stock-section'; import { ManualStockSection } from './manual-stock-section'; export const ProductInventorySection: React.FC = () => { - const { getInputProps, values } = useFormContext< Product >(); + const { getCheckboxControlProps, getInputProps, values } = + useFormContext< Product >(); const canManageStock = getAdminSetting( 'manageStock', 'yes' ) === 'yes'; return ( @@ -67,7 +68,7 @@ export const ProductInventorySection: React.FC = () => { 'washed-oxford-button-down-shirt', 'woocommerce' ) } - { ...getTextControlProps( getInputProps( 'sku' ) ) } + { ...getInputProps( 'sku' ) } /> { canManageStock && ( <> @@ -76,8 +77,9 @@ export const ProductInventorySection: React.FC = () => { 'Track quantity for this product', 'woocommerce' ) } - { ...getCheckboxProps( - getInputProps( 'manage_stock' ) + { ...getCheckboxControlProps( + 'manage_stock', + getCheckboxTracks( 'manage_stock' ) ) } /> { values.manage_stock && } diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx index 91fdc9e4bac..d1f1664ca42 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx @@ -32,7 +32,6 @@ import { } from '../fields/shipping-dimensions-image'; import { useProductHelper } from '../use-product-helper'; import { AddNewShippingClassModal } from '../shared/add-new-shipping-class-modal'; -import { getTextControlProps } from './utils'; import './product-shipping-section.scss'; import { ADD_NEW_SHIPPING_CLASS_OPTION_VALUE, @@ -74,7 +73,7 @@ function getInterpolatedSizeLabel( mixedString: string ) { * the first category different to `Uncategorized`. * * @see https://github.com/woocommerce/woocommerce/issues/34657 - * @param product The product + * @param product The product * @return The default shipping class */ function extractDefaultShippingClassFromProduct( @@ -94,7 +93,8 @@ function extractDefaultShippingClassFromProduct( export function ProductShippingSection( { product, }: ProductShippingSectionProps ) { - const { getInputProps, setValue } = useFormContext< PartialProduct >(); + const { getInputProps, getSelectControlProps, setValue } = + useFormContext< PartialProduct >(); const { formatNumber, parseNumber } = useProductHelper(); const [ highlightSide, setHighlightSide ] = useState< ShippingDimensionsImageProps[ 'highlight' ] >(); @@ -140,19 +140,29 @@ export function ProductShippingSection( { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ); - const selectShippingClassProps = getTextControlProps( - getInputProps( 'shipping_class' ) + const dimensionProps = { + onBlur: () => { + setHighlightSide( undefined ); + }, + sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) => + parseNumber( String( value ) ), + suffix: dimensionUnit, + }; + + const inputWidthProps = getInputProps( 'dimensions.width', dimensionProps ); + const inputLengthProps = getInputProps( + 'dimensions.length', + dimensionProps ); - const inputWidthProps = getTextControlProps( - getInputProps( 'dimensions.width' ) + const inputHeightProps = getInputProps( + 'dimensions.height', + dimensionProps ); - const inputLengthProps = getTextControlProps( - getInputProps( 'dimensions.length' ) - ); - const inputHeightProps = getTextControlProps( - getInputProps( 'dimensions.height' ) - ); - const inputWeightProps = getTextControlProps( getInputProps( 'weight' ) ); + const inputWeightProps = getInputProps( 'weight', { + sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) => + parseNumber( String( value ) ), + } ); + const shippingClassProps = getInputProps( 'shipping_class' ); return ( { if ( value === @@ -183,8 +187,14 @@ export function ProductShippingSection( { setShowShippingClassModal( true ); return; } - selectShippingClassProps?.onChange( value ); + shippingClassProps.onChange( value ); } } + options={ [ + ...DEFAULT_SHIPPING_CLASS_OPTIONS, + ...mapShippingClassToSelectOption( + shippingClasses ?? [] + ), + ] } /> { interpolateComponents( { @@ -235,7 +245,7 @@ export function ProductShippingSection( { - inputWidthProps?.onChange( - parseNumber( value ) - ) - } onFocus={ () => { setHighlightSide( 'A' ); } } - onBlur={ () => { - setHighlightSide( undefined ); - inputWidthProps?.onBlur(); - } } - suffix={ dimensionUnit } /> @@ -267,7 +267,7 @@ export function ProductShippingSection( { - inputLengthProps?.onChange( - parseNumber( value ) - ) - } onFocus={ () => { setHighlightSide( 'B' ); } } - onBlur={ () => { - setHighlightSide( undefined ); - inputLengthProps?.onBlur(); - } } - suffix={ dimensionUnit } /> @@ -299,7 +289,7 @@ export function ProductShippingSection( { - inputHeightProps?.onChange( - parseNumber( value ) - ) - } onFocus={ () => { setHighlightSide( 'C' ); } } - onBlur={ () => { - setHighlightSide( undefined ); - inputHeightProps?.onBlur(); - } } - suffix={ dimensionUnit } /> @@ -331,17 +311,12 @@ export function ProductShippingSection( { - inputWeightProps?.onChange( - parseNumber( value ) - ) - } suffix={ weightUnit } /> @@ -368,10 +343,10 @@ export function ProductShippingSection( { product && extractDefaultShippingClassFromProduct( product ) } - onAdd={ ( values ) => + onAdd={ ( shippingClassValues ) => createProductShippingClass< Promise< ProductShippingClass > - >( values ).then( ( value ) => { + >( shippingClassValues ).then( ( value ) => { invalidateResolution( 'getProductShippingClasses' ); setValue( 'shipping_class', value.slug ); return value; diff --git a/plugins/woocommerce-admin/client/products/sections/utils.ts b/plugins/woocommerce-admin/client/products/sections/utils.ts index a00789de2da..d2e6e418355 100644 --- a/plugins/woocommerce-admin/client/products/sections/utils.ts +++ b/plugins/woocommerce-admin/client/products/sections/utils.ts @@ -1,7 +1,8 @@ /** * External dependencies */ -import classnames from 'classnames'; +import { ChangeEvent } from 'react'; +import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; /** @@ -9,85 +10,67 @@ import { recordEvent } from '@woocommerce/tracks'; */ import { NUMBERS_AND_ALLOWED_CHARS } from '../constants'; -type gettersProps = { - context?: { - formatAmount: ( number: number | string ) => string; - getCurrencyConfig: () => { - code: string; - symbol: string; - symbolPosition: string; - decimalSeparator: string; - priceFormat: string; - thousandSeparator: string; - precision: number; - }; - }; - value: string; - name?: string; - checked: boolean; - selected?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChange: ( value: any ) => void; - onBlur: () => void; - className: string | undefined; - help: string | null | undefined; +type CurrencyConfig = { + code: string; + symbol: string; + symbolPosition: string; + decimalSeparator: string; + priceFormat: string; + thousandSeparator: string; + precision: number; }; -export const getCheckboxProps = ( { - checked = false, - className, - name, - onBlur, - onChange, -}: gettersProps ) => { +/** + * Get additional props to be passed to all checkbox inputs. + * + * @param {string} name Name of the checkbox + * @return {Object} Props. + */ +export const getCheckboxTracks = ( name: string ) => { return { - checked, - className: classnames( 'woocommerce-product__checkbox', className ), - onChange: ( isChecked: boolean ) => { + onChange: ( + isChecked: + | ChangeEvent< HTMLInputElement > + | Product[ keyof Product ] + ) => { recordEvent( `product_checkbox_${ name }`, { checked: isChecked, } ); - return onChange( isChecked ); }, - onBlur, }; }; -export const getTextControlProps = ( { - className, - onBlur, - onChange, - value = '', - help, -}: gettersProps ) => { - return { - value, - className: classnames( 'woocommerce-product__text', className ), - onChange, - onBlur, - help, - }; -}; - -export const getInputControlProps = ( { - className, - context, - onBlur, - onChange, - value = '', - help, -}: gettersProps ) => { - if ( ! context ) { - return; - } - const { formatAmount, getCurrencyConfig } = context; - const { decimalSeparator, symbol, symbolPosition, thousandSeparator } = - getCurrencyConfig(); +/** + * Get input props for currency related values and symbol positions. + * + * @param {Object} currencyConfig - Currency context + * @return {Object} Props. + */ +export const getCurrencySymbolProps = ( currencyConfig: CurrencyConfig ) => { + const { symbol, symbolPosition } = currencyConfig; const currencyPosition = symbolPosition.includes( 'left' ) ? 'prefix' : 'suffix'; - // Cleans the value to show. + return { + [ currencyPosition ]: symbol, + }; +}; + +/** + * Cleans and formats the currency value shown to the user. + * + * @param {string} value Form value. + * @param {Object} currencyConfig Currency context. + * @return {string} Display value. + */ +export const formatCurrencyDisplayValue = ( + value: string, + currencyConfig: CurrencyConfig, + format: ( number: number | string ) => string +) => { + const { decimalSeparator, thousandSeparator } = currencyConfig; + const regex = new RegExp( NUMBERS_AND_ALLOWED_CHARS.replace( '%s1', decimalSeparator ).replace( '%s2', @@ -95,16 +78,6 @@ export const getInputControlProps = ( { ), 'g' ); - const currencyString = - value === undefined - ? value - : formatAmount( value ).replace( regex, '' ); - return { - value: currencyString, - [ currencyPosition ]: symbol, - className: classnames( 'woocommerce-product__input', className ), - onChange, - onBlur, - help, - }; + + return value === undefined ? value : format( value ).replace( regex, '' ); }; diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx index 080e77127c5..b03359f8937 100644 --- a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx @@ -11,7 +11,6 @@ import { ProductShippingClass } from '@woocommerce/data'; /** * Internal dependencies */ -import { getTextControlProps } from '../../sections/utils'; import './add-new-shipping-class-modal.scss'; export type ShippingClassFormProps = { @@ -20,16 +19,10 @@ export type ShippingClassFormProps = { }; function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) { - const { getInputProps, isValidForm } = + const { errors, getInputProps, isValidForm } = useFormContext< ProductShippingClass >(); const [ isLoading, setIsLoading ] = useState( false ); - const inputNameProps = getTextControlProps( getInputProps( 'name' ) ); - const inputSlugProps = getTextControlProps( getInputProps( 'slug' ) ); - const inputDescriptionProps = getTextControlProps( - getInputProps( 'description' ) - ); - function handleAdd() { setIsLoading( true ); onAdd() @@ -45,11 +38,11 @@ function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) { return (
Date: Fri, 21 Oct 2022 12:34:27 -0300 Subject: [PATCH 045/149] Revert change that auto collapses the short description field (#35213) --- ...evert_closing_short_description_by_default | 4 ++++ .../admin/class-wc-admin-meta-boxes.php | 19 ------------------- 2 files changed, 4 insertions(+), 19 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default diff --git a/plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default b/plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default new file mode 100644 index 00000000000..87159e7afe8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Revert change that auto collapses the product short description field. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php index 24571dbd9dc..815613fadb4 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php @@ -23,11 +23,6 @@ class WC_Admin_Meta_Boxes { */ public const ERROR_STORE = 'woocommerce_meta_box_errors'; - /** - * The css class used to close the meta box - */ - private const CLOSED_CSS_CLASS = 'closed'; - /** * Is meta boxes saved once? * @@ -138,8 +133,6 @@ class WC_Admin_Meta_Boxes { add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' ); - add_filter( 'postbox_classes_product_postexcerpt', array( $this, 'collapse_postexcerpt' ) ); - // Orders. foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { $order_type_object = get_post_type_object( $type ); @@ -155,18 +148,6 @@ class WC_Admin_Meta_Boxes { } } - /** - * Collapse product short description meta box by default - * - * @param array $classes The css class array applied to the meta box. - */ - public function collapse_postexcerpt( $classes ) { - if ( ! in_array( self::CLOSED_CSS_CLASS, $classes, true ) ) { - array_push( $classes, self::CLOSED_CSS_CLASS ); - } - return $classes; - } - /** * Add default sort order for meta boxes on product page. */ From 64320a2c5cead883181eb0f35735b046b9531fc0 Mon Sep 17 00:00:00 2001 From: rodelgc Date: Sat, 22 Oct 2022 00:07:53 +0800 Subject: [PATCH 046/149] Migrate Daily smoke tests to Playwright (#35114) * Checkout updated files * Checkout more files * Remove invalid php-version input * Add changelog * Update pnpm-lock.yaml * Re-add accidentally deleted filter value * Remove unnecessary TODO comment --- .../scripts/prepare-test-summary-daily.js | 154 +++++++ .github/workflows/smoke-test-daily.yml | 403 ++++++++++++------ .../changelog/e2e-daily-tests-playwright | 4 + plugins/woocommerce/package.json | 2 + .../tests/api-core-tests/playwright.config.js | 33 +- .../woocommerce/tests/e2e-pw/global-setup.js | 52 ++- .../tests/e2e-pw/global-teardown.js | 19 +- .../tests/e2e-pw/playwright.config.js | 13 +- .../tests/merchant/order-emails.spec.js | 3 +- .../smoke-tests/update-woocommerce.spec.js | 121 ++++++ .../tests/smoke-tests/upload-plugin.spec.js | 104 +++++ .../tests/e2e-pw/utils/plugin-utils.js | 196 +++++++++ pnpm-lock.yaml | 60 +-- 13 files changed, 960 insertions(+), 204 deletions(-) create mode 100644 .github/workflows/scripts/prepare-test-summary-daily.js create mode 100644 plugins/woocommerce/changelog/e2e-daily-tests-playwright create mode 100644 plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js create mode 100644 plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js create mode 100644 plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js diff --git a/.github/workflows/scripts/prepare-test-summary-daily.js b/.github/workflows/scripts/prepare-test-summary-daily.js new file mode 100644 index 00000000000..784e42bf396 --- /dev/null +++ b/.github/workflows/scripts/prepare-test-summary-daily.js @@ -0,0 +1,154 @@ +/** + * Script to generate the test results summary. + */ +const { API_SUMMARY_PATH, E2E_PW_SUMMARY_PATH } = process.env; + +/** + * Convert the given `duration` from milliseconds to a more user-friendly string. + * For example, if `duration = 323000`, this function would return `5m 23s`. + * + * @param {Number} duration Duration in millisecods, as read from either the `summary.json` file in the Allure report, or from the `test-results.json` file from the Jest-Puppeteer report. + * @returns String in "5m 23s" format. + */ +const getFormattedDuration = ( duration ) => { + const durationMinutes = Math.floor( duration / 1000 / 60 ); + const durationSeconds = Math.floor( ( duration / 1000 ) % 60 ); + return `${ durationMinutes }m ${ durationSeconds }s`; +}; + +/** + * Extract the test report statistics (the number of tests that passed, failed, skipped, etc.) from Allure report's `summary.json` file. + * + * @param {string} summaryJSONPath Path to the Allure report's `summary.json` file. + * @returns An object containing relevant statistics from the Allure report. + */ +const getAllureSummaryStats = ( summaryJSONPath ) => { + const summary = require( summaryJSONPath ); + const { statistic, time } = summary; + const { passed, failed, skipped, broken, unknown, total } = statistic; + const { duration } = time; + + return { + passed, + failed, + skipped, + broken, + unknown, + total, + duration, + }; +}; + +/** + * Construct the array to be used for the API table row. + * + * @returns Array of API test result stats. + */ +const createAPITableRow = () => { + const { passed, failed, skipped, broken, unknown, total, duration } = + getAllureSummaryStats( API_SUMMARY_PATH ); + const durationFormatted = getFormattedDuration( duration ); + + return [ + 'API Tests', + passed.toString(), + failed.toString(), + broken.toString(), + skipped.toString(), + unknown.toString(), + total.toString(), + durationFormatted, + ]; +}; + +/** + * Construct the array to be used for the E2E table row. + * + * @returns Array of E2E test result stats. + */ +const createE2ETableRow = () => { + const { passed, failed, skipped, broken, unknown, total, duration } = + getAllureSummaryStats( E2E_PW_SUMMARY_PATH ); + const durationFormatted = getFormattedDuration( duration ); + + return [ + 'E2E Tests', + passed.toString(), + failed.toString(), + broken.toString(), + skipped.toString(), + unknown.toString(), + total.toString(), + durationFormatted, + ]; +}; + +/** + * Create the heading and test results table. + * + * @param core The GitHub Actions toolkit core object + */ +const addSummaryHeadingAndTable = ( core ) => { + const apiTableRow = createAPITableRow(); + const e2eTableRow = createE2ETableRow(); + + core.summary.addHeading( 'Smoke tests on trunk' ).addTable( [ + [ + { data: 'Test :test_tube:', header: true }, + { data: 'Passed :white_check_mark:', header: true }, + { data: 'Failed :rotating_light:', header: true }, + { data: 'Broken :construction:', header: true }, + { data: 'Skipped :next_track_button:', header: true }, + { data: 'Unknown :grey_question:', header: true }, + { data: 'Total :bar_chart:', header: true }, + { data: 'Duration :stopwatch:', header: true }, + ], + apiTableRow, + e2eTableRow, + ] ); +}; + +/** + * Add the summary footer. + * + * @param core The GitHub Actions toolkit core object + */ +const addSummaryFooter = ( core ) => { + core.summary + .addSeparator() + .addRaw( 'To view the full API test report, click ' ) + .addLink( + 'here.', + 'https://woocommerce.github.io/woocommerce-test-reports/daily/api' + ) + .addBreak() + .addRaw( 'To view the full E2E test report, click ' ) + .addLink( + 'here.', + 'https://woocommerce.github.io/woocommerce-test-reports/daily/e2e' + ) + .addBreak() + .addRaw( 'To view all test reports, visit the ' ) + .addLink( + 'WooCommerce Test Reports Dashboard.', + 'https://woocommerce.github.io/woocommerce-test-reports/' + ); +}; + +/** + * Generate the contents of the test results summary and post it on the workflow run. + * + * @param {*} params Objects passed from the calling GitHub Action workflow. + * @returns Stringified content of the test results summary. + */ +module.exports = async ( { core } ) => { + addSummaryHeadingAndTable( core ); + + addSummaryFooter( core ); + + const summary = core.summary.stringify(); + + await core.summary.write(); + + return summary; +}; diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 7c12d9f1f6c..7774735db14 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -1,99 +1,172 @@ name: Smoke test daily on: - schedule: - - cron: '25 3 * * *' + # schedule: + # - cron: '25 3 * * *' workflow_dispatch: +env: + API_ARTIFACT: api-daily--run-${{ github.run_number }} + E2E_ARTIFACT: e2e-daily--run-${{ github.run_number }} + FORCE_COLOR: 1 + BRANCH_NAME: ${{ github.ref_name }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - login-run: - name: Daily smoke test on trunk. + e2e-tests: + name: E2E tests on trunk runs-on: ubuntu-20.04 + if: always() env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report - outputs: - commit_message: ${{ steps.get_commit_message.outputs.commit_message }} + BASE_URL: ${{ secrets.SMOKE_TEST_URL }} + ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} + CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} + CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} + DEFAULT_TIMEOUT_OVERRIDE: 120000 steps: - uses: actions/checkout@v3 with: - ref: trunk + ref: ${{ env.BRANCH_NAME }} - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false - - name: Install Jest - run: npm install -g jest - - - name: Get latest commit message - id: get_commit_message - run: | - COMMIT_MESSAGE=$(git log --pretty=format:%s -1) - echo "::set-output name=commit_message::$COMMIT_MESSAGE" - - - name: Run E2E smoke test. + - name: Download and install Chromium browser. working-directory: plugins/woocommerce - env: - SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} - SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} - SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} - SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} - SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} - SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} - WC_E2E_SCREENSHOTS: 1 - E2E_RETEST: 1 - E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} - E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} - UPDATE_WC: 1 - DEFAULT_TIMEOUT_OVERRIDE: 120000 - run: | - pnpm exec wc-e2e docker:up - pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js - pnpm exec wc-e2e test:e2e + run: pnpm exec playwright install chromium - - name: Run API smoke tests + - name: Run 'Update WooCommerce' test. + working-directory: plugins/woocommerce + id: e2e-update + env: + UPDATE_WC: true + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js + + - name: Run the rest of E2E tests. + timeout-minutes: 60 + working-directory: plugins/woocommerce + id: e2e + env: + E2E_MAX_FAILURES: 15 + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js basic.spec.js + + - name: Generate Playwright E2E Test report. + id: generate_e2e_report + if: | + always() && + ( + steps.e2e-update.conclusion != 'cancelled' || + steps.e2e-update.conclusion != 'skipped' || + steps.e2e.conclusion != 'cancelled' || + steps.e2e.conclusion != 'skipped' + ) + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + + - name: Archive E2E test report + if: | + always() && + steps.generate_e2e_report.conclusion == 'success' + uses: actions/upload-artifact@v3 + with: + name: ${{ env.E2E_ARTIFACT }} + path: | + plugins/woocommerce/e2e/allure-results + plugins/woocommerce/e2e/allure-report + if-no-files-found: ignore + retention-days: 5 + + api-tests: + name: API tests on trunk + runs-on: ubuntu-20.04 + needs: [e2e-tests] + if: always() + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ env.BRANCH_NAME }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false + + - name: Run API tests. if: always() - id: run_api_tests + id: run_playwright_api_tests working-directory: plugins/woocommerce env: BASE_URL: ${{ secrets.SMOKE_TEST_URL }} USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} DEFAULT_TIMEOUT_OVERRIDE: 120000 - run: pnpm exec wc-api-tests test api + run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js + + - name: Generate API Test report. + id: generate_api_report + if: | + always() && + ( + steps.run_playwright_api_tests.conclusion != 'cancelled' || + steps.run_playwright_api_tests.conclusion != 'skipped' + ) + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report - name: Archive API test report if: | always() && - ( - steps.run_api_tests.conclusion != 'cancelled' || - steps.run_api_tests.conclusion != 'skipped' - ) + steps.generate_api_report.conclusion == 'success' uses: actions/upload-artifact@v3 with: - name: api-test-report---daily + name: ${{ env.API_ARTIFACT }} path: | - ${{ env.API_TEST_REPORT_DIR }}/allure-results - ${{ env.API_TEST_REPORT_DIR }}/allure-report + plugins/woocommerce/api-test-report/allure-results + plugins/woocommerce/api-test-report/allure-report + if-no-files-found: ignore retention-days: 5 + k6-tests: + name: k6 tests on trunk + runs-on: ubuntu-20.04 + needs: [api-tests] + if: always() + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ env.BRANCH_NAME }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false + + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + run: pnpm exec playwright install chromium + - name: Update performance test site with E2E test if: always() working-directory: plugins/woocommerce env: - SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/ - SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} - SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} - SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} - SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} - SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} - WC_E2E_SCREENSHOTS: 1 - E2E_RETEST: 1 - E2E_RETRY_TIMES: 0 - E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} - E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} - UPDATE_WC: 1 - DEFAULT_TIMEOUT_OVERRIDE: 120000 + BASE_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/ + ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} + ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} + CUSTOMER_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} + CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} + UPDATE_WC: true + DEFAULT_TIMEOUT_OVERRIDE: 120000 run: | - pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js + pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js continue-on-error: true - name: Install k6 @@ -114,34 +187,13 @@ jobs: run: | ./k6 run plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js - build: - name: Build zip for PR - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - build: false - - - name: Build zip - working-directory: plugins/woocommerce - run: bash bin/build-zip.sh - - - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: woocommerce - path: plugins/woocommerce/woocommerce.zip - retention-days: 7 - test-plugins: name: Smoke tests with ${{ matrix.plugin }} plugin installed runs-on: ubuntu-20.04 - needs: [build] + needs: [k6-tests] + if: always() + env: + USE_WP_ENV: 1 strategy: fail-fast: false matrix: @@ -160,59 +212,164 @@ jobs: - plugin: 'Contact Form 7' repo: 'takayukister/contact-form-7' steps: - - name: Create dirs. - run: | - mkdir -p package/woocommerce - mkdir -p tmp/woocommerce - - uses: actions/checkout@v3 with: - path: package/woocommerce + ref: ${{ env.BRANCH_NAME }} - - name: Download WooCommerce ZIP. - uses: actions/download-artifact@v3 - with: - name: woocommerce - path: tmp + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo - - name: Extract and replace WooCommerce zip. - working-directory: tmp - run: | - unzip woocommerce.zip -d . - rsync -a woocommerce/* ../package/woocommerce/plugins/woocommerce/ + - name: Launch wp-env e2e environment + working-directory: plugins/woocommerce + run: pnpm env:test --filter=woocommerce - - name: Load docker images and start containers. - working-directory: package/woocommerce - run: pnpm docker:up --filter=woocommerce + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + run: pnpm exec playwright install chromium - - name: Run tests command. - working-directory: package/woocommerce/plugins/woocommerce + - name: Run 'Upload plugin' test + id: e2e-upload + working-directory: plugins/woocommerce env: - WC_E2E_SCREENSHOTS: 1 - E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} - E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} PLUGIN_NAME: ${{ matrix.plugin }} GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} - run: | - pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js - pnpm exec wc-e2e test:e2e + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js upload-plugin.spec.js - publish-test-reports: - name: Publish test reports - if: always() + - name: Run the rest of E2E tests + id: e2e + working-directory: plugins/woocommerce + env: + E2E_MAX_FAILURES: 15 + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js basic.spec.js + + - name: Generate E2E Test report. + id: report + if: | + always() && + ( + steps.e2e-upload.conclusion != 'cancelled' || + steps.e2e-upload.conclusion != 'skipped' || + steps.e2e.conclusion != 'cancelled' || + steps.e2e.conclusion != 'skipped' + ) + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + + - name: Archive E2E test report + if: | + always() && + steps.report.conclusion == 'success' + uses: actions/upload-artifact@v3 + with: + name: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }}) + path: | + plugins/woocommerce/e2e/allure-results + plugins/woocommerce/e2e/allure-report + if-no-files-found: ignore + retention-days: 5 + + trunk-results: + name: Publish report on smoke tests on trunk + if: always() && + ! github.event.pull_request.head.repo.fork runs-on: ubuntu-20.04 - needs: [login-run, build, test-plugins] - env: - GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} - RUN_ID: ${{ github.run_id }} - API_ARTIFACT: api-test-report---daily - COMMIT_MESSAGE: ${{ needs.login-run.outputs.commit_message }} + needs: [test-plugins] steps: - - name: Publish API test report + - name: Create dirs + run: | + mkdir -p repo + mkdir -p artifacts/api + mkdir -p artifacts/e2e + mkdir -p output + + - name: Checkout code + uses: actions/checkout@v3 + with: + path: repo + ref: ${{ env.BRANCH_NAME }} + + - name: Download API test report artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.API_ARTIFACT }} + path: artifacts/api + + - name: Download E2E test report artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.E2E_ARTIFACT }} + path: artifacts/e2e + + - name: Post test summary + uses: actions/github-script@v6 + env: + API_SUMMARY_PATH: ${{ github.workspace }}/artifacts/api/allure-report/widgets/summary.json + E2E_PW_SUMMARY_PATH: ${{ github.workspace }}/artifacts/e2e/allure-report/widgets/summary.json + with: + result-encoding: string + script: | + const script = require( './repo/.github/workflows/scripts/prepare-test-summary-daily.js' ) + return await script( { core } ) + + - name: Publish report + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + RUN_ID: ${{ github.run_id }} run: | gh workflow run publish-test-reports-daily.yml \ -f run_id=$RUN_ID \ - -f api_artifact=$API_ARTIFACT \ - -f commit_message="$COMMIT_MESSAGE" \ + -f api_artifact="$API_ARTIFACT" \ + -f e2e_artifact="$E2E_ARTIFACT" \ + -f s3_root=public \ + --repo woocommerce/woocommerce-test-reports + + plugins-results: + name: Publish report on smoke tests with plugins + if: | + always() && + ! github.event.pull_request.head.repo.fork + runs-on: ubuntu-20.04 + needs: [test-plugins] + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + RUN_ID: ${{ github.run_id }} + ARTIFACT: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }}) + strategy: + fail-fast: false + matrix: + include: + - plugin: 'WooCommerce Payments' + repo: 'automattic/woocommerce-payments' + - plugin: 'WooCommerce PayPal Payments' + repo: 'woocommerce/woocommerce-paypal-payments' + - plugin: 'WooCommerce Shipping & Tax' + repo: 'automattic/woocommerce-services' + - plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo + repo: 'Yoast/wordpress-seo' + - plugin: 'Contact Form 7' + repo: 'takayukister/contact-form-7' + steps: + - name: Download test report artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.ARTIFACT }} + + # TODO: Add step to post job summary + + - name: Get slug + id: get-slug + uses: actions/github-script@v6 + with: + result-encoding: string + script: return "${{ matrix.repo }}".split( '/' ).pop() + + - name: Publish reports + run: | + gh workflow run publish-test-reports-daily-plugins.yml \ + -f run_id=$RUN_ID \ + -f artifact="${{ env.ARTIFACT }}" \ + -f plugin="${{ matrix.plugin }}" \ + -f slug="${{ steps.get-slug.outputs.result }}" \ + -f s3_root=public \ --repo woocommerce/woocommerce-test-reports diff --git a/plugins/woocommerce/changelog/e2e-daily-tests-playwright b/plugins/woocommerce/changelog/e2e-daily-tests-playwright new file mode 100644 index 00000000000..6edb1b9980b --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-daily-tests-playwright @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Enable Playwright tests on Daily Smoke Test workflow and upload its Allure reports to S3 bucket. \ No newline at end of file diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 6000ecf111b..73f6d14a4cd 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -68,12 +68,14 @@ "allure-commandline": "^2.17.2", "allure-playwright": "^2.0.0-beta.16", "autoprefixer": "9.8.6", + "axios": "^0.24.0", "babel-eslint": "10.1.0", "chai": "4.2.0", "chai-as-promised": "7.1.1", "config": "3.3.3", "cross-env": "6.0.3", "deasync": "0.1.26", + "dotenv": "^10.0.0", "eslint": "^8.12.0", "eslint-config-wpcalypso": "5.0.0", "eslint-plugin-jest": "23.20.0", diff --git a/plugins/woocommerce/tests/api-core-tests/playwright.config.js b/plugins/woocommerce/tests/api-core-tests/playwright.config.js index 460a6681668..caf5638cbb2 100644 --- a/plugins/woocommerce/tests/api-core-tests/playwright.config.js +++ b/plugins/woocommerce/tests/api-core-tests/playwright.config.js @@ -1,30 +1,27 @@ const { devices } = require( '@playwright/test' ); +const { + BASE_URL, + CI, + DEFAULT_TIMEOUT_OVERRIDE, + USER_KEY, + USER_SECRET, +} = process.env; require( 'dotenv' ).config(); -let baseURL = 'http://localhost:8086'; -let userKey = 'admin'; -let userSecret = 'password'; - -if ( process.env.BASE_URL ) { - baseURL = process.env.BASE_URL; -} - -if ( process.env.USER_KEY ) { - userKey = process.env.USER_KEY; -} - -if ( process.env.USER_SECRET ) { - userSecret = process.env.USER_SECRET; -} +const baseURL = BASE_URL ?? 'http://localhost:8086'; +const userKey = USER_KEY ?? 'admin'; +const userSecret = USER_SECRET ?? 'password'; const base64auth = btoa( `${ userKey }:${ userSecret }` ); const config = { - timeout: 90 * 1000, + timeout: DEFAULT_TIMEOUT_OVERRIDE + ? Number( DEFAULT_TIMEOUT_OVERRIDE ) + : 90 * 1000, expect: { timeout: 20 * 1000 }, outputDir: './report', testDir: 'tests', - retries: process.env.CI ? 4 : 2, + retries: CI ? 4 : 2, workers: 4, reporter: [ [ 'list' ], @@ -32,7 +29,7 @@ const config = { 'html', { outputFolder: 'output', - open: process.env.CI ? 'never' : 'always', + open: CI ? 'never' : 'always', }, ], [ diff --git a/plugins/woocommerce/tests/e2e-pw/global-setup.js b/plugins/woocommerce/tests/e2e-pw/global-setup.js index 5051c4fd28b..988aabc36a5 100644 --- a/plugins/woocommerce/tests/e2e-pw/global-setup.js +++ b/plugins/woocommerce/tests/e2e-pw/global-setup.js @@ -1,9 +1,18 @@ const { chromium, expect } = require( '@playwright/test' ); const fs = require( 'fs' ); +const { + ADMIN_USER, + ADMIN_PASSWORD, + CUSTOMER_USER, + CUSTOMER_PASSWORD, +} = process.env; +const adminUsername = ADMIN_USER ?? 'admin'; +const adminPassword = ADMIN_PASSWORD ?? 'password'; +const customerUsername = CUSTOMER_USER ?? 'customer'; +const customerPassword = CUSTOMER_PASSWORD ?? 'password'; module.exports = async ( config ) => { - const { stateDir } = config.projects[ 0 ].use; - const { baseURL } = config.projects[ 0 ].use; + const { stateDir, baseURL, userAgent } = config.projects[ 0 ].use; console.log( `State Dir: ${ stateDir }` ); console.log( `Base URL: ${ baseURL }` ); @@ -39,20 +48,27 @@ module.exports = async ( config ) => { let customerLoggedIn = false; let customerKeyConfigured = false; + // Specify user agent when running against an external test site to avoid getting HTTP 406 NOT ACCEPTABLE errors. + const contextOptions = { baseURL, userAgent }; + + // Create browser, browserContext, and page for customer and admin users const browser = await chromium.launch(); - const adminPage = await browser.newPage(); - const customerPage = await browser.newPage(); + const adminContext = await browser.newContext( contextOptions ); + const customerContext = await browser.newContext( contextOptions ); + const adminPage = await adminContext.newPage(); + const customerPage = await customerContext.newPage(); // Sign in as admin user and save state const adminRetries = 5; for ( let i = 0; i < adminRetries; i++ ) { try { console.log( 'Trying to log-in as admin...' ); - await adminPage.goto( `${ baseURL }/wp-admin` ); - await adminPage.fill( 'input[name="log"]', 'admin' ); - await adminPage.fill( 'input[name="pwd"]', 'password' ); + await adminPage.goto( `/wp-admin` ); + await adminPage.fill( 'input[name="log"]', adminUsername ); + await adminPage.fill( 'input[name="pwd"]', adminPassword ); await adminPage.click( 'text=Log In' ); - await adminPage.goto( `${ baseURL }/wp-admin` ); + await adminPage.waitForLoadState( 'networkidle' ); + await expect( adminPage.locator( 'div.wrap > h1' ) ).toHaveText( 'Dashboard' ); @@ -84,7 +100,7 @@ module.exports = async ( config ) => { try { console.log( 'Trying to add consumer token...' ); await adminPage.goto( - `${ baseURL }/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1` + `/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1` ); await adminPage.fill( '#key_description', 'Key for API access' ); await adminPage.selectOption( '#key_permissions', 'read_write' ); @@ -118,20 +134,22 @@ module.exports = async ( config ) => { for ( let i = 0; i < customerRetries; i++ ) { try { console.log( 'Trying to log-in as customer...' ); - await customerPage.goto( `${ baseURL }/wp-admin` ); - await customerPage.fill( 'input[name="log"]', 'customer' ); - await customerPage.fill( 'input[name="pwd"]', 'password' ); + await customerPage.goto( `/wp-admin` ); + await customerPage.fill( 'input[name="log"]', customerUsername ); + await customerPage.fill( 'input[name="pwd"]', customerPassword ); await customerPage.click( 'text=Log In' ); - await customerPage.goto( `${ baseURL }/my-account/` ); + await customerPage.goto( `/my-account` ); await expect( - customerPage.locator( 'h1.entry-title' ) - ).toContainText( 'My account' ); + customerPage.locator( + '.woocommerce-MyAccount-navigation-link--customer-logout' + ) + ).toBeVisible(); await expect( customerPage.locator( 'div.woocommerce-MyAccount-content > p >> nth=0' ) - ).toContainText( 'Jane Smith' ); + ).toContainText( 'Hello' ); await customerPage .context() @@ -154,5 +172,7 @@ module.exports = async ( config ) => { process.exit( 1 ); } + await adminContext.close(); + await customerContext.close(); await browser.close(); }; diff --git a/plugins/woocommerce/tests/e2e-pw/global-teardown.js b/plugins/woocommerce/tests/e2e-pw/global-teardown.js index 8469f034b93..f657db57ff9 100644 --- a/plugins/woocommerce/tests/e2e-pw/global-teardown.js +++ b/plugins/woocommerce/tests/e2e-pw/global-teardown.js @@ -1,10 +1,17 @@ const { chromium } = require( '@playwright/test' ); +const { ADMIN_USER, ADMIN_PASSWORD } = process.env; +const adminUsername = ADMIN_USER ?? 'admin'; +const adminPassword = ADMIN_PASSWORD ?? 'password'; module.exports = async ( config ) => { - const { baseURL } = config.projects[ 0 ].use; + const { baseURL, userAgent } = config.projects[ 0 ].use; + + // Specify user agent when running against an external test site to avoid getting HTTP 406 NOT ACCEPTABLE errors. + const contextOptions = { baseURL, userAgent }; const browser = await chromium.launch(); - const adminPage = await browser.newPage(); + const context = await browser.newContext( contextOptions ); + const adminPage = await context.newPage(); let consumerTokenCleared = false; @@ -13,12 +20,12 @@ module.exports = async ( config ) => { for ( let i = 0; i < keysRetries; i++ ) { try { console.log( 'Trying to clear consumer token... Try:' + i ); - await adminPage.goto( `${ baseURL }/wp-admin` ); - await adminPage.fill( 'input[name="log"]', 'admin' ); - await adminPage.fill( 'input[name="pwd"]', 'password' ); + await adminPage.goto( `/wp-admin` ); + await adminPage.fill( 'input[name="log"]', adminUsername ); + await adminPage.fill( 'input[name="pwd"]', adminPassword ); await adminPage.click( 'text=Log In' ); await adminPage.goto( - `${ baseURL }/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys` + `/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys` ); await adminPage.dispatchEvent( 'a.submitdelete', 'click' ); console.log( 'Cleared up consumer token successfully.' ); diff --git a/plugins/woocommerce/tests/e2e-pw/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/playwright.config.js index 849d550bc75..07db8f7bda0 100644 --- a/plugins/woocommerce/tests/e2e-pw/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/playwright.config.js @@ -1,8 +1,15 @@ const { devices } = require( '@playwright/test' ); -const { CI, E2E_MAX_FAILURES } = process.env; +const { + CI, + E2E_MAX_FAILURES, + BASE_URL, + DEFAULT_TIMEOUT_OVERRIDE, +} = process.env; const config = { - timeout: 90 * 1000, + timeout: DEFAULT_TIMEOUT_OVERRIDE + ? Number( DEFAULT_TIMEOUT_OVERRIDE ) + : 90 * 1000, expect: { timeout: 20 * 1000 }, outputDir: './report', globalSetup: require.resolve( './global-setup' ), @@ -28,7 +35,7 @@ const config = { video: 'on-first-retry', trace: 'retain-on-failure', viewport: { width: 1280, height: 720 }, - baseURL: 'http://localhost:8086', + baseURL: BASE_URL ?? 'http://localhost:8086', stateDir: 'e2e/storage/', }, projects: [ diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js index 3f3e653d904..e4dba4d13c3 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js @@ -1,4 +1,5 @@ const { test, expect } = require( '@playwright/test' ); +const { ADMIN_USER_EMAIL } = process.env; const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; test.describe( 'Merchant > Order Action emails received', () => { @@ -11,7 +12,7 @@ test.describe( 'Merchant > Order Action emails received', () => { const adminEmail = process.env.USE_WP_ENV === '1' ? 'wordpress@example.com' - : 'admin@woocommercecoree2etestsuite.com'; + : ADMIN_USER_EMAIL ?? 'admin@woocommercecoree2etestsuite.com'; const storeName = 'WooCommerce Core E2E Test Suite'; let orderId, newOrderId; diff --git a/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js new file mode 100644 index 00000000000..ba7d9d78a1e --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js @@ -0,0 +1,121 @@ +const { ADMINSTATE, UPDATE_WC, ADMIN_USER, ADMIN_PASSWORD } = process.env; +const { test, expect } = require( '@playwright/test' ); +const path = require( 'path' ); +const { + deletePlugin, + downloadZip, + deleteZip, +} = require( '../../utils/plugin-utils' ); + +const pluginZipPath = path.resolve( __dirname, '../../tmp/woocommerce.zip' ); + +test.describe( 'WooCommerce plugin can be uploaded and activated', () => { + // Skip test if UPDATE_WC is falsy. + test.skip( + ! Boolean( UPDATE_WC ), + `Skipping this test because UPDATE_WC is falsy: ${ UPDATE_WC }` + ); + + test.use( { storageState: ADMINSTATE } ); + + test.beforeAll( async () => { + // Download WooCommerce ZIP + await downloadZip( { + url: + 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip', + downloadPath: pluginZipPath, + } ); + } ); + + test.afterAll( async () => { + // Clean up downloaded zip + await deleteZip( pluginZipPath ); + } ); + + test( 'can upload and activate the WooCommerce plugin', async ( { + page, + playwright, + baseURL, + } ) => { + // Delete WooCommerce if it's installed. + await deletePlugin( { + request: playwright.request, + baseURL, + slug: 'woocommerce', + username: ADMIN_USER, + password: ADMIN_PASSWORD, + } ); + + // Open the plugin install page + await page.goto( 'wp-admin/plugin-install.php', { + waitUntil: 'networkidle', + } ); + + // Upload the plugin zip + await page.click( 'a.upload-view-toggle' ); + await expect( page.locator( 'p.install-help' ) ).toBeVisible(); + await expect( page.locator( 'p.install-help' ) ).toContainText( + 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' + ); + const [ fileChooser ] = await Promise.all( [ + page.waitForEvent( 'filechooser' ), + page.click( '#pluginzip' ), + ] ); + await fileChooser.setFiles( pluginZipPath ); + await page.click( '#install-plugin-submit' ); + await page.waitForLoadState( 'networkidle' ); + + // Activate the plugin + await page.click( '.button-primary' ); + await page.waitForLoadState( 'networkidle' ); + + // Go to 'Installed plugins' page + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + + // Assert that 'WooCommerce' is listed and active + await expect( + page.locator( '.plugin-title strong', { hasText: /^WooCommerce$/ } ) + ).toBeVisible(); + await expect( page.locator( '#deactivate-woocommerce' ) ).toBeVisible(); + } ); + + test( 'can run the database update', async ( { page } ) => { + const updateButton = page.locator( 'text=Update WooCommerce Database' ); + const updateCompleteMessage = page.locator( + 'text=WooCommerce database update complete.' + ); + + // Navigate to 'Installed Plugins' page + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + + // Skip this test if the "Update WooCommerce Database" button didn't appear. + test.skip( + ! ( await updateButton.isVisible() ), + 'The "Update WooCommerce Database" button did not appear after updating WooCommerce. Verify with the team if the WooCommerce version being tested does not really trigger a database update.' + ); + + // If the notice appears, start DB update + await updateButton.click(); + await page.waitForLoadState( 'networkidle' ); + + // Repeatedly reload the Plugins page up to 10 times until the message "WooCommerce database update complete." appears. + for ( + let reloads = 0; + reloads < 10 && ! ( await updateCompleteMessage.isVisible() ); + reloads++ + ) { + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + + // Wait 10s before the next reload. + await page.waitForTimeout( 10000 ); + } + + await expect( updateCompleteMessage ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js new file mode 100644 index 00000000000..a92195e7255 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js @@ -0,0 +1,104 @@ +const { + ADMINSTATE, + ADMIN_USER, + ADMIN_PASSWORD, + GITHUB_TOKEN, + PLUGIN_NAME, + PLUGIN_REPOSITORY, +} = process.env; +const { test, expect } = require( '@playwright/test' ); +const path = require( 'path' ); +const { + createPlugin, + deletePlugin, + downloadZip, + deleteZip, + getLatestReleaseZipUrl, +} = require( '../../utils/plugin-utils' ); + +const adminUsername = ADMIN_USER ?? 'admin'; +const adminPassword = ADMIN_PASSWORD ?? 'password'; + +let pluginZipPath; +let pluginSlug; + +test.describe( `${ PLUGIN_NAME } plugin can be uploaded and activated`, () => { + // Skip test if PLUGIN_REPOSITORY is falsy. + test.skip( + ! PLUGIN_REPOSITORY, + `Skipping this test because value of PLUGIN_REPOSITORY was falsy: ${ PLUGIN_REPOSITORY }` + ); + + test.use( { storageState: ADMINSTATE } ); + + test.beforeAll( async () => { + pluginSlug = PLUGIN_REPOSITORY.split( '/' ).pop(); + + // Get the download URL and filename of the plugin + const pluginDownloadURL = await getLatestReleaseZipUrl( { + repository: PLUGIN_REPOSITORY, + authorizationToken: GITHUB_TOKEN, + } ); + const zipFilename = pluginDownloadURL.split( '/' ).pop(); + pluginZipPath = path.resolve( __dirname, `../../tmp/${ zipFilename }` ); + + // Download the needed plugin. + await downloadZip( { + url: pluginDownloadURL, + downloadPath: pluginZipPath, + authToken: GITHUB_TOKEN, + } ); + } ); + + test.afterAll( async ( { baseURL, playwright } ) => { + // Delete the downloaded zip. + await deleteZip( pluginZipPath ); + + // Delete the plugin from the test site. + await deletePlugin( { + request: playwright.request, + baseURL, + slug: pluginSlug, + username: adminUsername, + password: adminPassword, + } ); + } ); + + test( `can upload and activate ${ PLUGIN_NAME }`, async ( { + page, + playwright, + baseURL, + } ) => { + // Delete the plugin if it's installed. + await deletePlugin( { + request: playwright.request, + baseURL, + slug: pluginSlug, + username: adminUsername, + password: adminPassword, + } ); + + // Install and activate plugin + await createPlugin( { + request: playwright.request, + baseURL, + slug: pluginSlug.split( '/' ).pop(), + username: adminUsername, + password: adminPassword, + } ); + + // Go to 'Installed plugins' page. + // Repeat in case the newly installed plugin redirects to their own onboarding screen upon first install, like what Yoast SEO does. + let reload = 2; + do { + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + } while ( ! page.url().includes( '/plugins.php' ) && --reload ); + + // Assert that the plugin is listed and active + await expect( + page.locator( `#deactivate-${ pluginSlug }` ) + ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js b/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js new file mode 100644 index 00000000000..69e84a7a6ac --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js @@ -0,0 +1,196 @@ +const { APIRequest } = require( '@playwright/test' ); +const axios = require( 'axios' ).default; +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * Encode basic auth username and password to be used in HTTP Authorization header. + * + * @param {string} username + * @param {string} password + * @returns Base64-encoded string + */ +const encodeCredentials = ( username, password ) => { + return Buffer.from( `${ username }:${ password }` ).toString( 'base64' ); +}; + +/** + * Deactivate and delete a plugin specified by the given `slug` using the WordPress API. + * + * @param {object} params + * @param {APIRequest} params.request + * @param {string} params.baseURL + * @param {string} params.slug + * @param {string} params.username + * @param {string} params.password + */ +export const deletePlugin = async ( { + request, + baseURL, + slug, + username, + password, +} ) => { + // Check if plugin is installed by getting the list of installed plugins, and then finding the one whose `textdomain` property equals `slug`. + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( username, password ) }`, + }, + } ); + const listPluginsResponse = await apiContext.get( + `/wp-json/wp/v2/plugins`, + { + failOnStatusCode: true, + } + ); + const pluginsList = await listPluginsResponse.json(); + const pluginToDelete = pluginsList.find( + ( { textdomain } ) => textdomain === slug + ); + + // If installed, get its `plugin` value and use it to deactivate and delete it. + if ( pluginToDelete ) { + const { plugin } = pluginToDelete; + + await apiContext.put( `/wp-json/wp/v2/plugins/${ plugin }`, { + data: { status: 'inactive' }, + failOnStatusCode: true, + } ); + + await apiContext.delete( `/wp-json/wp/v2/plugins/${ plugin }`, { + failOnStatusCode: true, + } ); + } +}; + +/** + * Download the zip file from a remote location. + * + * @param {object} param + * @param {string} param.url The URL where the zip file is located. + * @param {string} param.downloadPath The location where to download the zip to. + * @param {string} param.authToken Authorization token used to authenticate with the GitHub API if required. + */ +export const downloadZip = async ( { url, downloadPath, authToken } ) => { + // Create destination folder. + const dir = path.dirname( downloadPath ); + fs.mkdirSync( dir, { recursive: true } ); + + // Download the zip. + const options = { + url, + responseType: 'stream', + headers: { + 'user-agent': 'node.js', + }, + }; + + // If provided with a token, use it for authorization + if ( authToken ) { + options.headers.Authorization = `token ${ authToken }`; + } + + const response = await axios( options ); + response.data.pipe( fs.createWriteStream( downloadPath ) ); +}; + +/** + * Delete a zip file. Useful when cleaning up downloaded plugin zips. + * + * @param {string} zipFilePath Local file path to the ZIP. + */ +export const deleteZip = async ( zipFilePath ) => { + console.log( `Deleting file located in ${ zipFilePath }...` ); + await fs.unlink( zipFilePath, ( err ) => { + if ( err ) throw err; + console.log( `Successfully deleted!` ); + } ); +}; + +/** + * Get the download URL of the latest release zip for a plugin using GitHub's {@link https://docs.github.com/en/rest/releases/releases Releases API}. + * + * @param {{repository: string, authorizationToken: string, prerelease: boolean, perPage: number}} param + * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. + * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required. + * @param {boolean} prerelease Flag on whether to get a prelease or not. + * @param {number} perPage Limit of entries returned from the latest releases list, defaults to 3. + * @return {string} Download URL for the release zip file. + */ +export const getLatestReleaseZipUrl = async ( { + repository, + authorizationToken, + prerelease = false, + perPage = 3, +} ) => { + const requesturl = prerelease + ? `https://api.github.com/repos/${ repository }/releases?per_page=${ perPage }` + : `https://api.github.com/repos/${ repository }/releases/latest`; + + const options = { + url: requesturl, + headers: { 'user-agent': 'node.js' }, + }; + + // If provided with a token, use it for authorization + if ( authorizationToken ) { + options.headers.Authorization = `token ${ authorizationToken }`; + } + + // Call the List releases API endpoint in GitHub + const response = await axios( options ); + const body = response.data; + + // If it's a prerelease, find the first one and return its download URL. + if ( prerelease ) { + const latestPrerelease = body.find( ( { prerelease } ) => prerelease ); + + return latestPrerelease.assets[ 0 ].browser_download_url; + } else if ( authorizationToken ) { + // If it's a private repo, we need to download the archive this way. + // Use uploaded assets over downloading the zip archive. + if ( + body.assets && + body.assets.length > 0 && + body.assets[ 0 ].browser_download_url + ) { + return body.assets[ 0 ].browser_download_url; + } else { + const tagName = body.tag_name; + return `https://github.com/${ repository }/archive/${ tagName }.zip`; + } + } else { + return body.assets[ 0 ].browser_download_url; + } +}; + +/** + * Use the {@link https://developer.wordpress.org/rest-api/reference/plugins/#create-a-plugin Create plugin endpoint} to install and activate a plugin. + * + * @param {object} params + * @param {APIRequest} params.request + * @param {string} params.baseURL + * @param {string} params.slug + * @param {string} params.username + * @param {string} params.password + */ +export const createPlugin = async ( { + request, + baseURL, + slug, + username, + password, +} ) => { + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( username, password ) }`, + }, + } ); + + await apiContext.post( '/wp-json/wp/v2/plugins', { + data: { slug, status: 'active' }, + failOnStatusCode: true, + } ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c427cea03fb..adb76fe2165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1326,12 +1326,14 @@ importers: allure-commandline: ^2.17.2 allure-playwright: ^2.0.0-beta.16 autoprefixer: 9.8.6 + axios: ^0.24.0 babel-eslint: 10.1.0 chai: 4.2.0 chai-as-promised: 7.1.1 config: 3.3.3 cross-env: 6.0.3 deasync: 0.1.26 + dotenv: ^10.0.0 eslint: ^8.12.0 eslint-config-wpcalypso: 5.0.0 eslint-plugin-jest: 23.20.0 @@ -1370,12 +1372,14 @@ importers: allure-commandline: 2.18.1 allure-playwright: 2.0.0-beta.19 autoprefixer: 9.8.6 + axios: 0.24.0 babel-eslint: 10.1.0_eslint@8.25.0 chai: 4.2.0 chai-as-promised: 7.1.1_chai@4.2.0 config: 3.3.3 cross-env: 6.0.3 deasync: 0.1.26 + dotenv: 10.0.0 eslint: 8.25.0 eslint-config-wpcalypso: 5.0.0_44ie4thsizlsiqkjwba6wctao4 eslint-plugin-jest: 23.20.0_z4bbprzjrhnsfa24uvmcbu7f5q @@ -3558,7 +3562,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 + '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.12.9 '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color @@ -3571,7 +3575,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 + '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.16.12 '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color @@ -4361,7 +4365,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.16.12: @@ -4372,7 +4376,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.17.8: @@ -6532,11 +6536,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.19.0 - babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 - babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.16.12 - babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 semver: 6.3.0 transitivePeerDependencies: - supports-color @@ -6549,15 +6549,11 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-module-imports': 7.16.0 + '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.19.0 babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 - babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.17.8 semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true /@babel/plugin-transform-runtime/7.19.1_@babel+core@7.17.8: resolution: {integrity: sha512-2nJjTUFIzBMP/f/miLxEK9vxwW/KUXsdvN4sR//TmuDhe6yU2h57WmIOE12Gng3MDP/xpjUV/ToZRdcf8Yj4fA==} @@ -18808,7 +18804,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.14.5 + follow-redirects: 1.14.7 transitivePeerDependencies: - debug @@ -22557,7 +22553,6 @@ packages: /dotenv/10.0.0: resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} engines: {node: '>=10'} - dev: false /dotenv/8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} @@ -23265,11 +23260,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_lkzaig2qiyp6elizstfbgvzhie has: 1.0.3 - is-core-module: 2.10.0 + is-core-module: 2.8.0 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.0.4 object.values: 1.1.5 - resolve: 1.22.1 + resolve: 1.20.0 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -23327,11 +23322,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_54d5qjwnmqnp5634aqlesxatge has: 1.0.3 - is-core-module: 2.10.0 + is-core-module: 2.8.0 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.0.4 object.values: 1.1.5 - resolve: 1.22.1 + resolve: 1.20.0 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -25131,15 +25126,6 @@ packages: readable-stream: 2.3.7 dev: true - /follow-redirects/1.14.5: - resolution: {integrity: sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - /follow-redirects/1.14.7: resolution: {integrity: sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==} engines: {node: '>=4.0'} @@ -25264,7 +25250,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.16.7 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25275,7 +25261,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.7 + semver: 7.3.5 tapable: 1.1.3 typescript: 4.8.4 webpack: 5.70.0 @@ -25295,7 +25281,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.16.7 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25307,7 +25293,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.7 + semver: 7.3.5 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0_webpack-cli@3.3.12 @@ -25359,7 +25345,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.16.7 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25370,7 +25356,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.7 + semver: 7.3.5 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0 @@ -32184,7 +32170,6 @@ packages: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -41092,6 +41077,7 @@ packages: /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. dependencies: browser-process-hrtime: 1.0.0 From 569043ecdab63bbf2f14a82005049f17e2945efa Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Fri, 21 Oct 2022 18:32:55 +0100 Subject: [PATCH 047/149] add api-core-tests for system status (#35254) * add api-core-tests for system status * add api-core-tests for system status * add system status api-core-tests * add system status api-core-tests formatting updates --- ...dd-api-core-tests-system-status-crud-tests | 4 + .../system-status/system-status-crud.test.js | 623 ++++++++++++++++++ 2 files changed, 627 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests create mode 100644 plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js diff --git a/plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests new file mode 100644 index 00000000000..1a782cf170c --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for system status crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js new file mode 100644 index 00000000000..3d28851df59 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js @@ -0,0 +1,623 @@ +const { + test, + expect +} = require('@playwright/test'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group system status + * + */ +test.describe('System Status API tests', () => { + + test('can view all system status items', async ({ + request + }) => { + // call API to create a refund + const response = await request.get('/wp-json/wc/v3/system_status'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + environment: expect.objectContaining({ + "home_url": expect.any(String), + "site_url": expect.any(String), + "version": expect.any(String), + "log_directory": expect.any(String), + "log_directory_writable": expect.any(Boolean), + "wp_version": expect.any(String), + "wp_multisite": expect.any(Boolean), + "wp_memory_limit": expect.any(Number), + "wp_debug_mode": expect.any(Boolean), + "wp_cron": expect.any(Boolean), + "language": expect.any(String), + "external_object_cache": null, + "server_info": expect.any(String), + "php_version": expect.any(String), + "php_post_max_size": expect.any(Number), + "php_max_execution_time": expect.any(Number), + "php_max_input_vars": expect.any(Number), + "curl_version": expect.any(String), + "suhosin_installed": expect.any(Boolean), + "max_upload_size": expect.any(Number), + "mysql_version": expect.any(String), + "mysql_version_string": expect.any(String), + "default_timezone": expect.any(String), + "fsockopen_or_curl_enabled": expect.any(Boolean), + "soapclient_enabled": expect.any(Boolean), + "domdocument_enabled": expect.any(Boolean), + "gzip_enabled": expect.any(Boolean), + "mbstring_enabled": expect.any(Boolean), + "remote_post_successful": expect.any(Boolean), + "remote_post_response": expect.any(String), + "remote_get_successful": expect.any(Boolean), + "remote_get_response": expect.any(String), + }) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + database: expect.objectContaining({ + "wc_database_version": expect.any(String), + "database_prefix": expect.any(String), + "maxmind_geoip_database": expect.any(String), + "database_tables": { + "woocommerce": { + "wp_woocommerce_sessions": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_api_keys": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_attribute_taxonomies": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_downloadable_product_permissions": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_order_items": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_order_itemmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_tax_rates": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_tax_rate_locations": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_shipping_zones": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_shipping_zone_locations": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_shipping_zone_methods": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_payment_tokens": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_payment_tokenmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_log": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + } + }, + "other": { + "wp_actionscheduler_actions": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_actionscheduler_claims": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_actionscheduler_groups": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_actionscheduler_logs": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_commentmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_comments": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_links": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_options": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_postmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_posts": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_termmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_terms": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_term_relationships": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_term_taxonomy": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_usermeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_users": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_admin_notes": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_admin_note_actions": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_category_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_customer_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_download_log": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_order_coupon_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_order_product_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_order_stats": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_order_tax_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_product_attributes_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_product_download_directories": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_product_meta_lookup": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_rate_limits": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_reserved_stock": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_tax_rate_classes": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wc_webhooks": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_wpml_mails": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + } + } + }, + "database_size": { + "data": expect.any(Number), + "index": expect.any(Number) + } + }) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + active_plugins: expect.arrayContaining([{ + "plugin": expect.any(String), + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "url": expect.any(String), + "author_name": expect.any(String), + "author_url": expect.any(String), + "network_activated": expect.any(Boolean) + }, + { + "plugin": expect.any(String), + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "url": expect.any(String), + "author_name": expect.any(String), + "author_url": expect.any(String), + "network_activated": expect.any(Boolean) + }, + { + "plugin": expect.any(String), + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "url": expect.any(String), + "author_name": expect.any(String), + "author_url": expect.any(String), + "network_activated": expect.any(Boolean) + }, + { + "plugin": expect.any(String), + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "url": expect.any(String), + "author_name": expect.any(String), + "author_url": expect.any(String), + "network_activated": expect.any(Boolean) + } + ]) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + inactive_plugins: expect.arrayContaining([{ + "plugin": expect.any(String), + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "url": expect.any(String), + "author_name": expect.any(String), + "author_url": expect.any(String), + "network_activated": expect.any(Boolean) + }, + { + "plugin": expect.any(String), + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "url": expect.any(String), + "author_name": expect.any(String), + "author_url": expect.any(String), + "network_activated": expect.any(Boolean) + } + ]) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + dropins_mu_plugins: expect.objectContaining({ + "dropins": [], + "mu_plugins": [] + }) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + theme: expect.objectContaining({ + "name": expect.any(String), + "version": expect.any(String), + "version_latest": expect.any(String), + "author_url": expect.any(String), + "is_child_theme": expect.any(Boolean), + "has_woocommerce_support": expect.any(Boolean), + "has_woocommerce_file": expect.any(Boolean), + "has_outdated_templates": expect.any(Boolean), + "overrides": [], + "parent_name": expect.any(String), + "parent_version": expect.any(String), + "parent_version_latest": expect.any(String), + "parent_author_url": expect.any(String), + }) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + settings: expect.objectContaining({ + "api_enabled": expect.any(Boolean), + "force_ssl": expect.any(Boolean), + "currency": expect.any(String), + "currency_symbol": expect.any(String), + "currency_position": expect.any(String), + "thousand_separator": expect.any(String), + "decimal_separator": expect.any(String), + "number_of_decimals": expect.any(Number), + "geolocation_enabled": expect.any(Boolean), + "taxonomies": { + "external": expect.any(String), + "grouped": expect.any(String), + "simple": expect.any(String), + "variable": expect.any(String), + }, + "product_visibility_terms": { + "exclude-from-catalog": expect.any(String), + "exclude-from-search": expect.any(String), + "featured": expect.any(String), + "outofstock": expect.any(String), + "rated-1": expect.any(String), + "rated-2": expect.any(String), + "rated-3": expect.any(String), + "rated-4": expect.any(String), + "rated-5": expect.any(String), + }, + "woocommerce_com_connected": expect.any(String), + }) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + security: expect.objectContaining({ + "secure_connection": expect.any(Boolean), + "hide_errors": expect.any(Boolean) + }) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + pages: expect.arrayContaining([{ + "page_name": expect.any(String), + "page_id": expect.any(String), + "page_set": expect.any(Boolean), + "page_exists": expect.any(Boolean), + "page_visible": expect.any(Boolean), + "shortcode": expect.any(String), + "block": expect.any(String), + "shortcode_required": expect.any(Boolean), + "shortcode_present": expect.any(Boolean), + "block_present": expect.any(Boolean), + "block_required": expect.any(Boolean) + }, + { + "page_name": expect.any(String), + "page_id": expect.any(String), + "page_set": expect.any(Boolean), + "page_exists": expect.any(Boolean), + "page_visible": expect.any(Boolean), + "shortcode": expect.any(String), + "block": expect.any(String), + "shortcode_required": expect.any(Boolean), + "shortcode_present": expect.any(Boolean), + "block_present": expect.any(Boolean), + "block_required": expect.any(Boolean) + }, + { + "page_name": expect.any(String), + "page_id": expect.any(String), + "page_set": expect.any(Boolean), + "page_exists": expect.any(Boolean), + "page_visible": expect.any(Boolean), + "shortcode": expect.any(String), + "block": expect.any(String), + "shortcode_required": expect.any(Boolean), + "shortcode_present": expect.any(Boolean), + "block_present": expect.any(Boolean), + "block_required": expect.any(Boolean) + }, + { + "page_name": expect.any(String), + "page_id": expect.any(String), + "page_set": expect.any(Boolean), + "page_exists": expect.any(Boolean), + "page_visible": expect.any(Boolean), + "shortcode": expect.any(String), + "block": expect.any(String), + "shortcode_required": expect.any(Boolean), + "shortcode_present": expect.any(Boolean), + "block_present": expect.any(Boolean), + "block_required": expect.any(Boolean) + }, + { + "page_name": expect.any(String), + "page_id": expect.any(String), + "page_set": expect.any(Boolean), + "page_exists": expect.any(Boolean), + "page_visible": expect.any(Boolean), + "shortcode": expect.any(String), + "block": expect.any(String), + "shortcode_required": expect.any(Boolean), + "shortcode_present": expect.any(Boolean), + "block_present": expect.any(Boolean), + "block_required": expect.any(Boolean) + } + ]) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + post_type_counts: expect.arrayContaining([{ + "type": expect.any(String), + "count": expect.any(String), + }, + { + "type": expect.any(String), + "count": expect.any(String), + }, + { + "type": expect.any(String), + "count": expect.any(String), + } + ]) + }) + ); + }); + + test('can view all system status tools', async ({ + request + }) => { + // call API to create a refund + const response = await request.get('/wp-json/wc/v3/system_status/tools'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "clear_transients", + "name": "WooCommerce transients", + "action": "Clear transients", + "description": "This tool will clear the product/shop transients cache.", + }), + expect.objectContaining({ + "id": "clear_expired_transients", + "name": "Expired transients", + "action": "Clear transients", + "description": "This tool will clear ALL expired transients from WordPress.", + }), + expect.objectContaining({ + "id": "clear_expired_download_permissions", + "name": "Used-up download permissions", + "action": "Clean up download permissions", + "description": "This tool will delete expired download permissions and permissions with 0 remaining downloads.", + }), + expect.objectContaining({ + "id": "regenerate_product_lookup_tables", + "name": "Product lookup tables", + "action": "Regenerate", + "description": "This tool will regenerate product lookup table data. This process may take a while.", + }), + ]) + ); + + }); + + test('can retrieve a system status tool', async ({ + request + }) => { + // call API to create a refund + const response = await request.get('/wp-json/wc/v3/system_status/tools/clear_transients'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "clear_transients", + "name": "WooCommerce transients", + "action": "Clear transients", + "description": "This tool will clear the product/shop transients cache.", + }), + ); + }); + + test('can run a tool from system status', async ({ + request + }) => { + // call API to create a refund + const response = await request.put('/wp-json/wc/v3/system_status/tools/clear_transients'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "clear_transients", + "name": "WooCommerce transients", + "action": "Clear transients", + "description": "This tool will clear the product/shop transients cache.", + "success": true, + "message": "Product transients cleared", + }), + ); + }); + + +}); From a32966a2daf0ae1958a96582ac556b266f2e8562 Mon Sep 17 00:00:00 2001 From: Thomas Roberts <5656702+opr@users.noreply.github.com> Date: Fri, 21 Oct 2022 18:45:10 +0100 Subject: [PATCH 048/149] Update WooCommerce Blocks package to 8.7.4 (#35257) --- .../changelog/update-woocommerce-blocks-8.7.4 | 4 ++++ plugins/woocommerce/composer.json | 2 +- plugins/woocommerce/composer.lock | 14 +++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 new file mode 100644 index 00000000000..0688eb1f6b6 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Comment: Update WooCommerce Blocks to 8.7.4 diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index aabb8e10944..1da4b8966b2 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.4.2", - "woocommerce/woocommerce-blocks": "8.7.3" + "woocommerce/woocommerce-blocks": "8.7.4" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 308d3636793..d5d420dea28 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -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": "01f8810be9f7fc1f6404f4e02a59f10c", + "content-hash": "9d9a10e340f2c9d2366a082eb3b91344", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v8.7.3", + "version": "v8.7.4", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "6c3c5d5c7312bcd6c6fac1d9a96ae87141b52ae1" + "reference": "8553c41d5141725f2e399dab8527b573492402ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/6c3c5d5c7312bcd6c6fac1d9a96ae87141b52ae1", - "reference": "6c3c5d5c7312bcd6c6fac1d9a96ae87141b52ae1", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/8553c41d5141725f2e399dab8527b573492402ea", + "reference": "8553c41d5141725f2e399dab8527b573492402ea", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.3" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.4" }, - "time": "2022-10-20T17:09:09+00:00" + "time": "2022-10-21T15:55:49+00:00" } ], "packages-dev": [ From da2c81b8b1dc2e8e1ed69062c45f749e2d7e6a19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:47:24 -0500 Subject: [PATCH 049/149] Delete changelog files based on PR 35257 (#35261) Delete changelog files for 35257 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 deleted file mode 100644 index 0688eb1f6b6..00000000000 --- a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.4 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Comment: Update WooCommerce Blocks to 8.7.4 From a94eecc910ba01c23494d8fba856e39bff27df7a Mon Sep 17 00:00:00 2001 From: Jonathan Lane Date: Fri, 21 Oct 2022 14:16:00 -0700 Subject: [PATCH 050/149] Playwright 1.27.1 (#35106) * Update Playwright to 1.27.1 * Add changelog * Try adding retry action to github actions workflow * Revert "Try adding retry action to github actions workflow" This reverts commit 6196a5dd21e6bceb97518b247967fb828a0f9b30. Co-authored-by: Jon Lane --- .../woocommerce/changelog/playwright-1_27_1 | 4 +++ plugins/woocommerce/package.json | 3 +-- pnpm-lock.yaml | 27 ++++++------------- 3 files changed, 13 insertions(+), 21 deletions(-) create mode 100644 plugins/woocommerce/changelog/playwright-1_27_1 diff --git a/plugins/woocommerce/changelog/playwright-1_27_1 b/plugins/woocommerce/changelog/playwright-1_27_1 new file mode 100644 index 00000000000..77fabbb7835 --- /dev/null +++ b/plugins/woocommerce/changelog/playwright-1_27_1 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Update Playwright from 1.26.1 to 1.27.1 diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 73f6d14a4cd..bddcf4c5ef0 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -49,7 +49,7 @@ "@babel/core": "7.12.9", "@babel/preset-env": "7.12.7", "@babel/register": "7.12.1", - "@playwright/test": "^1.26.1", + "@playwright/test": "^1.27.1", "@typescript-eslint/eslint-plugin": "3.10.1", "@typescript-eslint/experimental-utils": "3.10.1", "@typescript-eslint/parser": "3.10.1", @@ -82,7 +82,6 @@ "istanbul": "1.0.0-alpha.2", "jest": "^27.5.1", "mocha": "7.2.0", - "playwright": "^1.26.1", "prettier": "npm:wp-prettier@2.0.5", "stylelint": "^13.8.0", "typescript": "^4.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb76fe2165..e63fca6e913 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1307,7 +1307,7 @@ importers: '@babel/core': 7.12.9 '@babel/preset-env': 7.12.7 '@babel/register': 7.12.1 - '@playwright/test': ^1.26.1 + '@playwright/test': ^1.27.1 '@typescript-eslint/eslint-plugin': 3.10.1 '@typescript-eslint/experimental-utils': 3.10.1 '@typescript-eslint/parser': 3.10.1 @@ -1340,7 +1340,6 @@ importers: istanbul: 1.0.0-alpha.2 jest: ^27.5.1 mocha: 7.2.0 - playwright: ^1.26.1 prettier: npm:wp-prettier@2.0.5 stylelint: ^13.8.0 typescript: ^4.8.3 @@ -1353,7 +1352,7 @@ importers: '@babel/core': 7.12.9 '@babel/preset-env': 7.12.7_@babel+core@7.12.9 '@babel/register': 7.12.1_@babel+core@7.12.9 - '@playwright/test': 1.26.1 + '@playwright/test': 1.27.1 '@typescript-eslint/eslint-plugin': 3.10.1_s5hr7yeqqy6e4q6twdgyz7l2pu '@typescript-eslint/experimental-utils': 3.10.1_z4bbprzjrhnsfa24uvmcbu7f5q '@typescript-eslint/parser': 3.10.1_z4bbprzjrhnsfa24uvmcbu7f5q @@ -1386,7 +1385,6 @@ importers: istanbul: 1.0.0-alpha.2 jest: 27.5.1 mocha: 7.2.0 - playwright: 1.26.1 prettier: /wp-prettier/2.0.5 stylelint: 13.13.1 typescript: 4.8.4 @@ -9671,13 +9669,13 @@ packages: dependencies: '@octokit/openapi-types': 13.10.0 - /@playwright/test/1.26.1: - resolution: {integrity: sha512-bNxyZASVt2adSZ9gbD7NCydzcb5JaI0OR9hc7s+nmPeH604gwp0zp17NNpwXY4c8nvuBGQQ9oGDx72LE+cUWvw==} + /@playwright/test/1.27.1: + resolution: {integrity: sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==} engines: {node: '>=14'} hasBin: true dependencies: '@types/node': 17.0.21 - playwright-core: 1.26.1 + playwright-core: 1.27.1 dev: true /@pmmmwh/react-refresh-webpack-plugin/0.5.1_a3gyllrqvxpec3fpybsrposvju: @@ -33849,21 +33847,12 @@ packages: find-up: 5.0.0 dev: true - /playwright-core/1.26.1: - resolution: {integrity: sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==} + /playwright-core/1.27.1: + resolution: {integrity: sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==} engines: {node: '>=14'} hasBin: true dev: true - /playwright/1.26.1: - resolution: {integrity: sha512-WQmEdCgYYe8jOEkhkW9QLcK0PB+w1RZztBLYIT10MEEsENYg251cU0IzebDINreQsUt+HCwwRhtdz4weH9ICcQ==} - engines: {node: '>=14'} - hasBin: true - requiresBuild: true - dependencies: - playwright-core: 1.26.1 - dev: true - /plur/4.0.0: resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==} engines: {node: '>=10'} @@ -39387,7 +39376,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.8.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 transitivePeerDependencies: - acorn From 0c5f93bb39170d3a8d419f7d87e969e386f31588 Mon Sep 17 00:00:00 2001 From: Tam Mullen Date: Fri, 21 Oct 2022 22:58:36 +0100 Subject: [PATCH 051/149] k6: add baseline scenario and additional account requests (#35252) Add additional my account k6 requests and updated test scenarios --- .../changelog/add-k6-baseline-test | 5 + .../requests/shopper/my-account-orders.js | 107 ++++++ .../tests/gh-action-daily-ext-requests.js | 115 +++--- .../tests/gh-action-pr-requests.js | 11 + .../performance/tests/simple-all-requests.js | 119 ++++--- .../performance/tests/wc-baseline-load.js | 328 ++++++++++++++++++ 6 files changed, 579 insertions(+), 106 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-k6-baseline-test create mode 100644 plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js create mode 100644 plugins/woocommerce/tests/performance/tests/wc-baseline-load.js diff --git a/plugins/woocommerce/changelog/add-k6-baseline-test b/plugins/woocommerce/changelog/add-k6-baseline-test new file mode 100644 index 00000000000..9d95650169c --- /dev/null +++ b/plugins/woocommerce/changelog/add-k6-baseline-test @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Change to k6 test scenario + + diff --git a/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js new file mode 100644 index 00000000000..3ec046b709e --- /dev/null +++ b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js @@ -0,0 +1,107 @@ +/* eslint-disable no-shadow */ +/* eslint-disable import/no-unresolved */ +/** + * External dependencies + */ +import { sleep, check, group } from 'k6'; +import http from 'k6/http'; +import { + randomIntBetween, + findBetween, +} from 'https://jslib.k6.io/k6-utils/1.1.0/index.js'; + +/** + * Internal dependencies + */ +import { base_url, think_time_min, think_time_max } from '../../config.js'; +import { + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders, +} from '../../headers.js'; + +export function myAccountOrders() { + let response; + let my_account_order_id; + + group( 'My Account', function () { + const requestHeaders = Object.assign( + {}, + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders + ); + + response = http.get( `${ base_url }/my-account`, { + headers: requestHeaders, + tags: { name: 'Shopper - My Account' }, + } ); + check( response, { + 'is status 200': ( r ) => r.status === 200, + 'body contains: my account welcome message': ( response ) => + response.body.includes( + 'From your account dashboard you can view' + ), + } ); + } ); + + sleep( randomIntBetween( `${ think_time_min }`, `${ think_time_max }` ) ); + + group( 'My Account Orders', function () { + const requestHeaders = Object.assign( + {}, + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders + ); + + response = http.get( `${ base_url }/my-account/orders/`, { + headers: requestHeaders, + tags: { name: 'Shopper - My Account Orders' }, + } ); + check( response, { + 'is status 200': ( r ) => r.status === 200, + "body contains: 'Orders' title": ( response ) => + response.body.includes( '>Orders' ), + } ); + my_account_order_id = findBetween( + response.body, + 'my-account/view-order/', + '/">' + ); + } ); + + sleep( randomIntBetween( `${ think_time_min }`, `${ think_time_max }` ) ); + + group( 'My Account Open Order', function () { + const requestHeaders = Object.assign( + {}, + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders + ); + + response = http.get( + `${ base_url }/my-account/view-order/${ my_account_order_id }`, + { + headers: requestHeaders, + tags: { name: 'Shopper - My Account Open Order' }, + } + ); + check( response, { + 'is status 200': ( r ) => r.status === 200, + "body contains: 'Order number' title": ( response ) => + response.body.includes( `${ my_account_order_id }` ), + } ); + } ); + + sleep( randomIntBetween( `${ think_time_min }`, `${ think_time_max }` ) ); +} + +export default function () { + myAccountOrders(); +} diff --git a/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js b/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js index d6b56104371..01e670e7bb8 100644 --- a/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js +++ b/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js @@ -10,6 +10,7 @@ import { cartRemoveItem } from '../requests/shopper/cart-remove-item.js'; import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; import { myAccount } from '../requests/shopper/my-account.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; import { categoryPage } from '../requests/shopper/category-page.js'; import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; import { products } from '../requests/merchant/products.js'; @@ -82,156 +83,165 @@ export const options = { }, }, thresholds: { - checks: [ 'rate==1' ], + checks: ['rate==1'], 'http_req_duration{name:Shopper - Site Root}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Shop Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Search Products}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Category Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Product Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=add_to_cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Remove Item From Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=apply_coupon}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Update Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=update_order_review}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Order Received}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=get_refreshed_fragments}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - My Account Login Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to My Account}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, ], 'http_req_duration{name:Merchant - WP Login Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Login to WP Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - WC-Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/orders?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/reviews?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/low-in-stock?}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - All Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Completed Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - New Order Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Create New Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Open Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update Existing Order Status}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Email}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Address}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - Filter Orders By Month}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Filter Orders By Customer}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - All Products}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Add New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=sample-permalink}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=heartbeat autosave}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Coupons}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/onboarding/tasks?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/admin/notes?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/options?options=woocommerce_ces_tracks_queue}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - action=heartbeat}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:API - Create Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Retrieve Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Update Order (Status)}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Delete Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Create Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Update (Status) Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], }, }; @@ -253,6 +263,7 @@ export function checkoutCustomerLoginFlow() { } export function myAccountFlow() { myAccount(); + myAccountOrders(); } export function cartFlow() { cartRemoveItem(); diff --git a/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js b/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js index b8049ec1152..ac98b0330f6 100644 --- a/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js +++ b/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js @@ -10,6 +10,7 @@ import { cartRemoveItem } from '../requests/shopper/cart-remove-item.js'; import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; import { myAccount } from '../requests/shopper/my-account.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; import { categoryPage } from '../requests/shopper/category-page.js'; import { wpLogin } from '../requests/merchant/wp-login.js'; import { products } from '../requests/merchant/products.js'; @@ -138,6 +139,15 @@ export const options = { 'http_req_duration{name:Shopper - Login to My Account}': [ `${ shopper_request_threshold }`, ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, + ], 'http_req_duration{name:Merchant - WP Login Page}': [ `${ merchant_request_threshold }`, ], @@ -254,6 +264,7 @@ export function checkoutCustomerLoginFlow() { } export function myAccountFlow() { myAccount(); + myAccountOrders(); } export function cartFlow() { cartRemoveItem(); diff --git a/plugins/woocommerce/tests/performance/tests/simple-all-requests.js b/plugins/woocommerce/tests/performance/tests/simple-all-requests.js index a2a9c5762f3..6eb654c15a0 100644 --- a/plugins/woocommerce/tests/performance/tests/simple-all-requests.js +++ b/plugins/woocommerce/tests/performance/tests/simple-all-requests.js @@ -11,6 +11,7 @@ import { cartApplyCoupon } from '../requests/shopper/cart-apply-coupon.js'; import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; import { myAccount } from '../requests/shopper/my-account.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; import { categoryPage } from '../requests/shopper/category-page.js'; import { products } from '../requests/merchant/products.js'; import { addProduct } from '../requests/merchant/add-product.js'; @@ -73,7 +74,7 @@ export const options = { allMerchantSmoke: { executor: 'per-vu-iterations', vus: 1, - iterations: 1, + iterations: 2, maxDuration: '360s', exec: 'allMerchantFlow', }, @@ -86,156 +87,165 @@ export const options = { }, }, thresholds: { - checks: [ 'rate==1' ], + checks: ['rate==1'], 'http_req_duration{name:Shopper - Site Root}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Shop Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Search Products}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Category Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Product Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=add_to_cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Remove Item From Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=apply_coupon}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Update Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=update_order_review}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Order Received}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=get_refreshed_fragments}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - My Account Login Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to My Account}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, ], 'http_req_duration{name:Merchant - WP Login Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Login to WP Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - WC-Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/orders?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/reviews?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/low-in-stock?}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - All Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Completed Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - New Order Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Create New Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Open Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update Existing Order Status}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Email}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Address}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - Filter Orders By Month}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Filter Orders By Customer}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - All Products}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Add New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=sample-permalink}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=heartbeat autosave}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Coupons}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/onboarding/tasks?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/admin/notes?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/options?options=woocommerce_ces_tracks_queue}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - action=heartbeat}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:API - Create Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Retrieve Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Update Order (Status)}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Delete Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Create Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Update (Status) Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], }, }; @@ -257,6 +267,7 @@ export function checkoutCustomerLoginFlow() { } export function myAccountFlow() { myAccount(); + myAccountOrders(); } export function cartFlow() { cartRemoveItem(); @@ -264,7 +275,7 @@ export function cartFlow() { } export function allMerchantFlow() { - if ( admin_acc_login === true ) { + if (admin_acc_login === true) { myAccountMerchantLogin(); } else { wpLogin(); diff --git a/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js b/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js new file mode 100644 index 00000000000..d2ae164f612 --- /dev/null +++ b/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js @@ -0,0 +1,328 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable no-undef */ +/** + * Internal dependencies + */ +import { homePage } from '../requests/shopper/home.js'; +import { shopPage } from '../requests/shopper/shop-page.js'; +import { searchProduct } from '../requests/shopper/search-product.js'; +import { singleProduct } from '../requests/shopper/single-product.js'; +import { cart } from '../requests/shopper/cart.js'; +import { cartRemoveItem } from '../requests/shopper/cart-remove-item.js'; +import { cartApplyCoupon } from '../requests/shopper/cart-apply-coupon.js'; +import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; +import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; +import { categoryPage } from '../requests/shopper/category-page.js'; +import { products } from '../requests/merchant/products.js'; +import { addProduct } from '../requests/merchant/add-product.js'; +import { coupons } from '../requests/merchant/coupons.js'; +import { orders } from '../requests/merchant/orders.js'; +import { ordersSearch } from '../requests/merchant/orders-search.js'; +import { ordersFilter } from '../requests/merchant/orders-filter.js'; +import { addOrder } from '../requests/merchant/add-order.js'; +import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; +import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; +import { ordersAPI } from '../requests/api/orders.js'; +import { admin_acc_login } from '../config.js'; + +const shopper_request_threshold = 'p(95)<100000'; +const merchant_request_threshold = 'p(95)<100000'; +const api_request_threshold = 'p(95)<100000'; + +export const options = { + scenarios: { + merchantOrders: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'merchantOrderFlows', + }, + merchantOther: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'merchantOtherFlows', + }, + shopperBrowsing: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 10, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'shopperBrowsingFlows', + }, + shopperGuestCheckouts: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'checkoutGuestFlow', + }, + shopperCustomerCheckouts: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'checkoutCustomerLoginFlow', + }, + apiBackground: { + executor: 'ramping-arrival-rate', + startRate: 1, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 5, + stages: [ + // target value is iterations per timeUnit + { target: 1, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'allAPIFlow', + }, + }, + thresholds: { + 'http_req_duration{name:Shopper - Site Root}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Shop Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Search Products}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Category Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Product Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=add_to_cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - View Cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Remove Item From Cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=apply_coupon}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Update Cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - View Checkout}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=update_order_review}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=checkout}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Order Received}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=get_refreshed_fragments}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Login to Checkout}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Login Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Login to My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Merchant - WP Login Page}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Login to WP Admin}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - WC-Admin}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/orders?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/products/reviews?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/products/low-in-stock?}': + [`${merchant_request_threshold}`], + 'http_req_duration{name:Merchant - All Orders}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Completed Orders}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - New Order Page}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Create New Order}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Open Order}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Update Existing Order Status}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Search Orders By Product}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Search Orders By Customer Email}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Search Orders By Customer Address}': + [`${merchant_request_threshold}`], + 'http_req_duration{name:Merchant - Filter Orders By Month}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Filter Orders By Customer}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - All Products}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Add New Product}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - action=sample-permalink}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - action=heartbeat autosave}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Update New Product}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Coupons}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-admin/onboarding/tasks?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/admin/notes?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-admin/options?options=woocommerce_ces_tracks_queue}': + [`${merchant_request_threshold}`], + 'http_req_duration{name:Merchant - action=heartbeat}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:API - Create Order}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Retrieve Order}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Update Order (Status)}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Delete Order}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Batch Create Orders}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Batch Update (Status) Orders}': [ + `${api_request_threshold}`, + ], + }, +}; + +// Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. +export function merchantOrderFlows() { + if (admin_acc_login === true) { + myAccountMerchantLogin(); + } else { + wpLogin(); + } + addOrder(); + orders(); + ordersSearch(); + ordersFilter(); +} + +// Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. +export function merchantOtherFlows() { + if (admin_acc_login === true) { + myAccountMerchantLogin(); + } else { + wpLogin(); + } + homeWCAdmin(); + addProduct(); + products(); + coupons(); +} +export function shopperBrowsingFlows() { + homePage(); + shopPage(); + searchProduct(); + singleProduct(); + cartRemoveItem(); + cartApplyCoupon(); + categoryPage(); +} +export function checkoutGuestFlow() { + cart(); + checkoutGuest(); +} +export function checkoutCustomerLoginFlow() { + cart(); + checkoutCustomerLogin(); + myAccountOrders(); +} +export function allAPIFlow() { + ordersAPI(); +} From 64ee20ff0f47fc69c546eefeaa421536c4fa3a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Sat, 22 Oct 2022 10:47:17 -0300 Subject: [PATCH 052/149] Show a dismissible snackbar if the server responds an error (#35160) * Show a dismissible snackbar if the server responds an error * Removed the default value New shipping class from the Name field and replaced it with a placeholder: e.g. Fragile products * Only used the category's name when the user creates a new shipping class for the first time * Fix linter errors * Update grammar error Co-authored-by: Joshua T Flowers * Add empty initial values to form field to prevent controlled/uncontrolled react error * Tune error handling Co-authored-by: Joshua T Flowers --- .../layout/transient-notices/style.scss | 2 +- .../sections/product-shipping-section.tsx | 64 ++++-- .../test/product-shipping-section.spec.tsx | 187 +++++++++++++++--- .../add-new-shipping-class-modal.tsx | 6 +- .../changelog/add-35037-error-handling | 4 + 5 files changed, 214 insertions(+), 49 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-35037-error-handling diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/style.scss b/plugins/woocommerce-admin/client/layout/transient-notices/style.scss index 25dfad3a708..ce44b0bd7a9 100644 --- a/plugins/woocommerce-admin/client/layout/transient-notices/style.scss +++ b/plugins/woocommerce-admin/client/layout/transient-notices/style.scss @@ -4,7 +4,7 @@ position: fixed; bottom: $gap-small; left: $admin-menu-width + $gap; - z-index: 99999; + z-index: calc(z-index('.components-snackbar-list') + 1); width: auto; @media ( max-width: 960px ) { diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx index d1f1664ca42..679d6e433e9 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx @@ -42,6 +42,10 @@ export type ProductShippingSectionProps = { product?: PartialProduct; }; +type ServerErrorResponse = { + code: string; +}; + export const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ { value: '', label: __( 'No shipping class', 'woocommerce' ) }, { @@ -70,19 +74,26 @@ function getInterpolatedSizeLabel( mixedString: string ) { /** * This extracts a shipping class from the product categories. Using - * the first category different to `Uncategorized`. + * the first category different to `Uncategorized` and check if the + * category was not added to the shipping class list * * @see https://github.com/woocommerce/woocommerce/issues/34657 - * @param product The product + * @see https://github.com/woocommerce/woocommerce/issues/35037 + * @param product The product + * @param shippingClasses The shipping classes * @return The default shipping class */ function extractDefaultShippingClassFromProduct( - product: PartialProduct + product?: PartialProduct, + shippingClasses?: ProductShippingClass[] ): Partial< ProductShippingClass > | undefined { const category = product?.categories?.find( ( { slug } ) => slug !== UNCATEGORIZED_CATEGORY_SLUG ); - if ( category ) { + if ( + category && + ! shippingClasses?.some( ( { slug } ) => slug === category.slug ) + ) { return { name: category.name, slug: category.slug, @@ -139,6 +150,7 @@ export function ProductShippingSection( { const { createProductShippingClass, invalidateResolution } = useDispatch( EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ); + const { createErrorNotice } = useDispatch( 'core/notices' ); const dimensionProps = { onBlur: () => { @@ -164,6 +176,28 @@ export function ProductShippingSection( { } ); const shippingClassProps = getInputProps( 'shipping_class' ); + function handleShippingClassServerError( + error: ServerErrorResponse + ): Promise< ProductShippingClass > { + let message = __( + 'We couldn’t add this shipping class. Try again in a few seconds.', + 'woocommerce' + ); + + if ( error.code === 'term_exists' ) { + message = __( + 'A shipping class with that slug already exists.', + 'woocommerce' + ); + } + + createErrorNotice( message, { + explicitDismiss: true, + } ); + + throw error; + } + return ( createProductShippingClass< Promise< ProductShippingClass > - >( shippingClassValues ).then( ( value ) => { - invalidateResolution( 'getProductShippingClasses' ); - setValue( 'shipping_class', value.slug ); - return value; - } ) + >( shippingClassValues ) + .then( ( value ) => { + invalidateResolution( + 'getProductShippingClasses' + ); + setValue( 'shipping_class', value.slug ); + return value; + } ) + .catch( handleShippingClassServerError ) } onCancel={ () => setShowShippingClassModal( false ) } /> diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx index 23f2ac8b4ef..235853ac4d8 100644 --- a/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx +++ b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx @@ -4,6 +4,8 @@ import { act, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useSelect, useDispatch } from '@wordpress/data'; +import { PartialProduct, ProductShippingClass } from '@woocommerce/data'; +import { __ } from '@wordpress/i18n'; import { Form } from '@woocommerce/components'; /** @@ -20,60 +22,183 @@ jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn(), } ) ); +function getShippingClassDialog() { + return screen.getByRole( 'dialog' ); +} + +function getShippingClassSelect() { + return screen.getByLabelText( __( 'Shipping class', 'woocommerce' ) ); +} + +async function getShippingClassNameInput() { + const dialog = getShippingClassDialog(); + return within( dialog ).getByLabelText( __( 'Name', 'woocommerce' ) ); +} + +async function getShippingClassSlugInput() { + const dialog = getShippingClassDialog(); + return within( dialog ).getByLabelText( 'Slug', { exact: false } ); +} + +async function openShippingClassDialog() { + const select = getShippingClassSelect(); + + await act( async () => + userEvent.selectOptions( select, ADD_NEW_SHIPPING_CLASS_OPTION_VALUE ) + ); + + return getShippingClassDialog(); +} + +async function submitShippingClassDialog() { + const dialog = getShippingClassDialog(); + const buttonAdd = within( dialog ).getByText( __( 'Add', 'woocommerce' ) ); + await act( async () => userEvent.click( buttonAdd ) ); +} + +async function addNewShippingClass( name?: string, slug?: string ) { + await openShippingClassDialog(); + + if ( name ) { + const inputName = await getShippingClassNameInput(); + userEvent.type( inputName, name ); + } + + if ( slug ) { + const inputSlug = await getShippingClassSlugInput(); + userEvent.type( inputSlug, slug ); + } + + await submitShippingClassDialog(); +} + describe( 'ProductShippingSection', () => { const useSelectMock = useSelect as jest.Mock; const useDispatchMock = useDispatch as jest.Mock; + const createProductShippingClass = jest.fn(); + const invalidateResolution = jest.fn(); + const createErrorNotice = jest.fn(); + let shippingClasses: Partial< ProductShippingClass >[]; beforeEach( () => { + shippingClasses = []; + + useSelectMock.mockReturnValue( { + shippingClasses, + hasResolvedShippingClasses: true, + } ); + + useDispatchMock.mockReturnValue( { + createProductShippingClass, + invalidateResolution, + createErrorNotice, + } ); + } ); + + afterEach( () => { jest.clearAllMocks(); } ); describe( 'when creating a product', () => { + beforeEach( () => { + render( +
+ + + ); + } ); + describe( 'when creating a shipping class', () => { const newShippingClass = { name: 'New shipping class', slug: 'new-shipping-class', }; - const createProductShippingClass = jest - .fn() - .mockReturnValue( Promise.resolve( newShippingClass ) ); - const invalidateResolution = jest.fn(); - - beforeEach( () => { - useSelectMock.mockReturnValue( { - shippingClasses: [ newShippingClass ], - hasResolvedShippingClasses: true, - } ); - - useDispatchMock.mockReturnValue( { - createProductShippingClass, - invalidateResolution, - } ); - - render( -
- - - ); - } ); it( 'should be selected as the current option', async () => { - const select = screen.getByLabelText( 'Shipping class' ); - act( () => - userEvent.selectOptions( - select, - ADD_NEW_SHIPPING_CLASS_OPTION_VALUE - ) + createProductShippingClass.mockImplementation( ( value ) => { + shippingClasses.push( value ); + return Promise.resolve( value ); + } ); + + await addNewShippingClass( + newShippingClass.name, + newShippingClass.slug ); - const dialog = screen.getByRole( 'dialog' ); - const addButton = within( dialog ).getByText( 'Add' ); - await act( async () => userEvent.click( addButton ) ); + const select = getShippingClassSelect(); expect( select ).toHaveDisplayValue( [ newShippingClass.name, ] ); } ); + + it( 'should show a snackbar message when server responds an error', async () => { + createProductShippingClass.mockRejectedValue( + new Error( 'Server Error' ) + ); + + await addNewShippingClass( newShippingClass.name ); + + expect( createErrorNotice ).toHaveBeenNthCalledWith( + 1, + __( + 'We couldn’t add this shipping class. Try again in a few seconds.', + 'woocommerce' + ), + expect.objectContaining( { explicitDismiss: true } ) + ); + } ); + } ); + } ); + + describe( 'when editing a product', () => { + const product: PartialProduct = { + id: 1, + categories: [ + { + id: 1, + name: 'Category 1', + slug: 'category-1', + }, + ], + }; + + beforeEach( () => { + render( +
+ + + ); + } ); + + describe( 'when creating a shipping class', () => { + it( 'should add the first cat as a shipping class only once', async () => { + const category = product?.categories?.at( 0 ); + const newShippingClass: Partial< ProductShippingClass > = { + name: category?.name, + slug: category?.slug, + }; + createProductShippingClass.mockImplementation( ( value ) => { + shippingClasses.push( value ); + return Promise.resolve( value ); + } ); + + await openShippingClassDialog(); + + let inputName = await getShippingClassNameInput(); + expect( inputName ).toHaveValue( newShippingClass.name ); + + await submitShippingClassDialog(); + expect( createProductShippingClass ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( newShippingClass ) + ); + + await openShippingClassDialog(); + + inputName = await getShippingClassNameInput(); + expect( inputName ).not.toHaveValue( newShippingClass.name ); + } ); } ); } ); } ); diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx index b03359f8937..21962919ec2 100644 --- a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx @@ -40,6 +40,7 @@ function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) { void; }; -const INITIAL_VALUES = { - name: __( 'New shipping class', 'woocommerce' ), - slug: __( 'new-shipping-class', 'woocommerce' ), -}; +const INITIAL_VALUES = { name: '', slug: '', description: '' }; export function AddNewShippingClassModal( { shippingClass, diff --git a/plugins/woocommerce/changelog/add-35037-error-handling b/plugins/woocommerce/changelog/add-35037-error-handling new file mode 100644 index 00000000000..7df34f3d8c1 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35037-error-handling @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Show a dismissible snackbar if the server responds with an error From a72e68ea6503580d89f2d1b4c66306a4c90fa4f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 16:37:07 +0800 Subject: [PATCH 053/149] Prepare Packages for Release (#35277) Automated change: Prep @woocommerce/components for release. Co-authored-by: chihsuan --- packages/js/components/CHANGELOG.md | 22 ++++++++------- packages/js/components/changelog/update-34996 | 4 --- packages/js/components/package.json | 2 +- pnpm-lock.yaml | 27 +++++-------------- 4 files changed, 21 insertions(+), 34 deletions(-) delete mode 100644 packages/js/components/changelog/update-34996 diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md index 39efd837868..a8bb8684c2a 100644 --- a/packages/js/components/CHANGELOG.md +++ b/packages/js/components/CHANGELOG.md @@ -2,15 +2,25 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [11.1.0](https://www.npmjs.com/package/@woocommerce/components/v/11.1.0) - 2022-10-24 + +- Minor - Allow passing of additional props to form inputs [#35160] + ## [11.0.0](https://www.npmjs.com/package/@woocommerce/components/v/11.0.0) - 2022-10-20 - Patch - Export StepperProps for external usage [#35140] - Patch - Fixed the initial setting of DateTimePickerControl's input field. [#35140] -- Minor - Fix Enriched-label styles - #34382 [#35140] - Patch - Fix EnrichedLabel Storybook story styles so they don't affect other stories. [#35140] - Patch - Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes. [#35140] -- Minor - Fix initially selected items in SelectControl component [#35140] - Patch - Fix issue with form onChange handler, passing outdated values. [#35140] +- Patch - Update tag component styling [#35140] +- Patch - Add missing type definitions and add babel config for tests [#35140] +- Patch - Merging trunk with local [#35140] +- Patch - Removed unfinished and unused SplitDropdown component. [#35140] +- Patch - Assume ambiguous dates passed into DateTimePickerControl are UTC. [#35140] +- Patch - Remove default selected sortable item. [#35140] +- Minor - Fix Enriched-label styles +- Minor - Fix initially selected items in SelectControl component [#35140] - Minor - Add date-only mode to DateTimePickerControl. [#35140] - Minor - Add disabled option to the Select Control input component and alter the onInputChange callback [#35140] - Minor - Add form input name dot notation name="product.dimensions.width" [#35140] @@ -23,7 +33,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Minor - Add rich text editor component [#35140] - Minor - Add SortableList component [#35140] - Minor - Allow external tags in SelectControl component [#35140] -- Major [ **BREAKING CHANGE** ] - Create new experimental SelectControl component [#35140] - Minor - Export ImportProps type. Add DateTimePickerControl to Form stories and tests. [#35140] - Minor - Images Product management [#35140] - Minor - Remove EnrichedLabel component in favor of Tooltip component [#35140] @@ -34,22 +43,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Minor - Add support in SelectControl for using the popover slot for the popover. [#35140] - Minor - Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift. [#35140] - Minor - Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options. [#35140] -- Patch - Update tag component styling [#35140] - Minor - Update text input placement in SelectControl [#35140] - Minor - Add component EnrichedLabel #34214 [#35140] -- Patch - Add missing type definitions and add babel config for tests [#35140] - Minor - Add new shippping class modal to a shipping class section in product page [#35140] - Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#35140] - Minor - Fix node and pnpm versions via engines [#35140] -- Patch - Merging trunk with local [#35140] -- Patch - Removed unfinished and unused SplitDropdown component. [#35140] - Minor - Update Plugin installer component to TS [#35140] - Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35140] -- Patch - Assume ambiguous dates passed into DateTimePickerControl are UTC. [#35140] - Minor - Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). [#35140] -- Patch - Remove default selected sortable item. [#35140] - Minor - Improve experimental SelectControl accessibility [#35140] - Minor - Improve Sortable component acessibility [#35140] +- - Create new experimental SelectControl component [#35140] ## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12 diff --git a/packages/js/components/changelog/update-34996 b/packages/js/components/changelog/update-34996 deleted file mode 100644 index 5b25d4c8856..00000000000 --- a/packages/js/components/changelog/update-34996 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Allow passing of additional props to form inputs diff --git a/packages/js/components/package.json b/packages/js/components/package.json index d6dc1577e07..3955a228b76 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/components", - "version": "11.0.0", + "version": "11.1.0", "description": "UI components for WooCommerce.", "author": "Automattic", "license": "GPL-3.0-or-later", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e63fca6e913..cb65b4dd2c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2878,24 +2878,6 @@ packages: - supports-color dev: true - /@babel/helper-define-polyfill-provider/0.3.0_@babel+core@7.16.12: - resolution: {integrity: sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==} - peerDependencies: - '@babel/core': ^7.4.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/traverse': 7.19.3 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/helper-define-polyfill-provider/0.3.0_@babel+core@7.17.8: resolution: {integrity: sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==} peerDependencies: @@ -6534,7 +6516,11 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 + '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 + babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.16.12 + babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 semver: 6.3.0 transitivePeerDependencies: - supports-color @@ -6552,6 +6538,7 @@ packages: babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 semver: 6.3.0 + dev: true /@babel/plugin-transform-runtime/7.19.1_@babel+core@7.17.8: resolution: {integrity: sha512-2nJjTUFIzBMP/f/miLxEK9vxwW/KUXsdvN4sR//TmuDhe6yU2h57WmIOE12Gng3MDP/xpjUV/ToZRdcf8Yj4fA==} @@ -19259,7 +19246,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.16.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.16.12 core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color @@ -39376,7 +39363,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.8.0 - webpack: 5.70.0_webpack-cli@3.3.12 + webpack: 5.70.0 transitivePeerDependencies: - acorn From 87942e776cffff109aad686deb79ce5e7fbe9467 Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:26:18 +0100 Subject: [PATCH 054/149] add payment gateway api-core-tests (#35279) --- ...api-core-tests-payment-gateways-crud-tests | 4 + .../payment-gateways-crud.test.js | 241 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests create mode 100644 plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js diff --git a/plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests new file mode 100644 index 00000000000..d578dc26973 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for payment gateways crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js new file mode 100644 index 00000000000..fb7ef68bf0a --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js @@ -0,0 +1,241 @@ +const { + test, + expect +} = require('@playwright/test'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group payment gateways + * + */ +test.describe('Payment Gateways API tests', () => { + + test('can view all payment gatways', async ({ + request + }) => { + // call API to retrieve the payment gateways + const response = await request.get('/wp-json/wc/v3/payment_gateways'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "bacs", + "title": "Direct bank transfer", + "description": "Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.", + "order": "", + "enabled": false, + "method_title": "Direct bank transfer", + "method_description": "Take payments in person via BACS. More commonly known as direct bank/wire transfer.", + "method_supports": [ + "products" + ], + "settings": { + "title": { + "id": "title", + "label": "Title", + "description": "This controls the title which the user sees during checkout.", + "type": "safe_text", + "value": "Direct bank transfer", + "default": "Direct bank transfer", + "tip": "This controls the title which the user sees during checkout.", + "placeholder": "" + }, + "instructions": { + "id": "instructions", + "label": "Instructions", + "description": "Instructions that will be added to the thank you page and emails.", + "type": "textarea", + "value": "", + "default": "", + "tip": "Instructions that will be added to the thank you page and emails.", + "placeholder": "" + } + }, + }), + + expect.objectContaining({ + "id": "cheque", + "title": "Check payments", + "description": "Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.", + "order": "", + "enabled": false, + "method_title": "Check payments", + "method_description": "Take payments in person via checks. This offline gateway can also be useful to test purchases.", + "method_supports": [ + "products" + ], + "settings": { + "title": { + "id": "title", + "label": "Title", + "description": "This controls the title which the user sees during checkout.", + "type": "safe_text", + "value": "Check payments", + "default": "Check payments", + "tip": "This controls the title which the user sees during checkout.", + "placeholder": "" + }, + "instructions": { + "id": "instructions", + "label": "Instructions", + "description": "Instructions that will be added to the thank you page and emails.", + "type": "textarea", + "value": "", + "default": "", + "tip": "Instructions that will be added to the thank you page and emails.", + "placeholder": "" + } + }, + }), + + expect.objectContaining({ + "id": "cod", + "title": "Cash on delivery", + "description": "Pay with cash upon delivery.", + "order": "", + "enabled": false, + "method_title": "Cash on delivery", + "method_description": "Have your customers pay with cash (or by other means) upon delivery.", + "method_supports": [ + "products" + ], + "settings": { + "title": { + "id": "title", + "label": "Title", + "description": "Payment method description that the customer will see on your checkout.", + "type": "safe_text", + "value": "Cash on delivery", + "default": "Cash on delivery", + "tip": "Payment method description that the customer will see on your checkout.", + "placeholder": "" + }, + "instructions": { + "id": "instructions", + "label": "Instructions", + "description": "Instructions that will be added to the thank you page.", + "type": "textarea", + "value": "Pay with cash upon delivery.", + "default": "Pay with cash upon delivery.", + "tip": "Instructions that will be added to the thank you page.", + "placeholder": "" + }, + "enable_for_methods": { + "id": "enable_for_methods", + "label": "Enable for shipping methods", + "description": "If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.", + "type": "multiselect", + "value": "", + "default": "", + "tip": "If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.", + "placeholder": "", + "options": { + "Flat rate": { + "flat_rate": "Any "Flat rate" method" + }, + "Free shipping": { + "free_shipping": "Any "Free shipping" method" + }, + "Local pickup": { + "local_pickup": "Any "Local pickup" method" + } + } + }, + "enable_for_virtual": { + "id": "enable_for_virtual", + "label": "Accept COD if the order is virtual", + "description": "", + "type": "checkbox", + "value": "yes", + "default": "yes", + "tip": "", + "placeholder": "" + } + }, + }) + + ]) + ); + }); + + test('can view a payment gateway', async ({ + request + }) => { + // call API to retrieve a single payment gatway + const response = await request.get('/wp-json/wc/v3/payment_gateways/bacs'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "bacs", + "title": "Direct bank transfer", + "description": "Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.", + "order": "", + "enabled": false, + "method_title": "Direct bank transfer", + "method_description": "Take payments in person via BACS. More commonly known as direct bank/wire transfer.", + "method_supports": [ + "products" + ], + "settings": { + "title": { + "id": "title", + "label": "Title", + "description": "This controls the title which the user sees during checkout.", + "type": "safe_text", + "value": "Direct bank transfer", + "default": "Direct bank transfer", + "tip": "This controls the title which the user sees during checkout.", + "placeholder": "" + }, + "instructions": { + "id": "instructions", + "label": "Instructions", + "description": "Instructions that will be added to the thank you page and emails.", + "type": "textarea", + "value": "", + "default": "", + "tip": "Instructions that will be added to the thank you page and emails.", + "placeholder": "" + } + }, + })); + }); + + test('can update a payment gateway', async ({ + request + }) => { + // call API to update a payment gatway + const response = await request.put('/wp-json/wc/v3/payment_gateways/bacs', { + data: { + enabled: true + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + enabled: true + }), + ); + + // reset payment gateway setting + await request.put('/wp-json/wc/v3/payment_gateways/bacs', { + data: { + enabled: false + } + }); + }); +}); From c6313abfd1c7f722dbfdd0b12ffbf8f9a30d2e06 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Mon, 24 Oct 2022 10:35:18 -0500 Subject: [PATCH 055/149] Update the config for assignment of community PRs (#35161) * Update project-community-pr-assigner.yml * Remove extraneous comment, add beta-tester to assignment config * Update .github/project-community-pr-assigner.yml Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> * Update .github/project-community-pr-assigner.yml Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> * Update .github/project-community-pr-assigner.yml Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> * Update .github/project-community-pr-assigner.yml Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> * Update .github/project-community-pr-assigner.yml Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> * Update .github/project-community-pr-assigner.yml Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> --- .github/project-community-pr-assigner.yml | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml index e0f2f4e12a2..7454a24859d 100644 --- a/.github/project-community-pr-assigner.yml +++ b/.github/project-community-pr-assigner.yml @@ -1,3 +1,85 @@ +# See https://github.com/shufo/auto-assign-reviewer-by-files/blob/main/README.md for configuration format ".github/*": - team: atlas + +"packages/js/api/**/*": + - team: solaris + +"packages/js/e2e-utils/**/*": + - team: solaris + +"packages/js/e2e-environment/**/*": + - team: solaris + +"packages/js/api-core-tests/**/*": + - team: solaris + +"packages/js/e2e-core-tests/**/*": + - team: solaris + +"packages/js/admin-e2e-tests/**/*": + - team: solaris + +"packages/js/components/**/*": + - team: mothra + - team: ghidorah + +"packages/js/csv-export/**/*": + - team: mothra + +"packages/js/currency/**/*": + - team: mothra + +"packages/js/customer-effort-score/**/*": + - team: mothra + +"packages/js/data/**/*": + - team: mothra + - team: ghidorah + +"packages/js/date/**/*": + - team: mothra + +"packages/js/dependency-extraction-webpack-plugin/**/*": + - team: mothra + +"packages/js/eslint-plugin/**/*": + - team: mothra + +"packages/js/experimental/**/*": + - team: mothra + +"packages/js/explat/**/*": + - team: mothra + - team: ghidorah + +"packages/js/navigation/**/*": + - team: mothra + +"packages/js/number/**/*": + - team: mothra + +"packages/js/onboarding/**/*": + - team: ghidorah + +"packages/js/tracks/**/*": + - team: mothra + +"plugins/woocommerce/**/*": + - team: proton + +"plugins/woocommerce/src/Admin/**/*": + - team: mothra + - team: ghidorah + +"plugins/woocommerce/src/Internal/Admin/**/*": + - team: mothra + - team: ghidorah + +"plugins/woocommerce-admin/**/*": + - team: mothra + - team: ghidorah + +"plugins/woocommerce-beta-tester/**/*": + - team: atlas From 2aa4ce0d9f64a618a01b6f77d2af12b9c592011c Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Mon, 24 Oct 2022 09:08:27 -0700 Subject: [PATCH 056/149] Disable product inventory toggle when inventory management is disabled (#35059) * Disable product inventory toggle when inventory management is disabled * Export conditional wrapper as experimental component * Conditionally show the tooltip * Add comment explaining the tooltip overlay * Add components changelog entry * Display tooltip on hover any portion of toggle or label * Add changelog entry * Fix scss lint error * Center tooltip over label and toggle * Fix up input props after rebase * Add wrapper around field to maintain block item formatting --- packages/js/components/changelog/add-35046 | 4 ++ .../conditional-wrapper.tsx | 0 .../{util => }/conditional-wrapper/index.ts | 0 .../src/image-gallery/image-gallery-item.tsx | 2 +- packages/js/components/src/index.ts | 1 + packages/js/components/src/util/index.ts | 1 - .../layout/product-section-layout.scss | 8 ++++ .../client/products/product-page.scss | 24 ++++++++++ .../product-inventory-section.tsx | 45 +++++++++++++++---- plugins/woocommerce/changelog/add-35046 | 4 ++ 10 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 packages/js/components/changelog/add-35046 rename packages/js/components/src/{util => }/conditional-wrapper/conditional-wrapper.tsx (100%) rename packages/js/components/src/{util => }/conditional-wrapper/index.ts (100%) delete mode 100644 packages/js/components/src/util/index.ts create mode 100644 plugins/woocommerce/changelog/add-35046 diff --git a/packages/js/components/changelog/add-35046 b/packages/js/components/changelog/add-35046 new file mode 100644 index 00000000000..e9d7806593d --- /dev/null +++ b/packages/js/components/changelog/add-35046 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add experimental ConditionalWrapper component diff --git a/packages/js/components/src/util/conditional-wrapper/conditional-wrapper.tsx b/packages/js/components/src/conditional-wrapper/conditional-wrapper.tsx similarity index 100% rename from packages/js/components/src/util/conditional-wrapper/conditional-wrapper.tsx rename to packages/js/components/src/conditional-wrapper/conditional-wrapper.tsx diff --git a/packages/js/components/src/util/conditional-wrapper/index.ts b/packages/js/components/src/conditional-wrapper/index.ts similarity index 100% rename from packages/js/components/src/util/conditional-wrapper/index.ts rename to packages/js/components/src/conditional-wrapper/index.ts diff --git a/packages/js/components/src/image-gallery/image-gallery-item.tsx b/packages/js/components/src/image-gallery/image-gallery-item.tsx index 5c781929e0b..8a775661722 100644 --- a/packages/js/components/src/image-gallery/image-gallery-item.tsx +++ b/packages/js/components/src/image-gallery/image-gallery-item.tsx @@ -9,7 +9,7 @@ import { createElement, Fragment } from '@wordpress/element'; */ import Pill from '../pill'; import { SortableHandle, NonSortableItem } from '../sortable'; -import { ConditionalWrapper } from '../util/conditional-wrapper'; +import { ConditionalWrapper } from '../conditional-wrapper'; export type ImageGalleryItemProps = { id?: string; diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 58f2182ad5a..38a03508a74 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -4,6 +4,7 @@ export { default as AnimationSlider } from './animation-slider'; export { default as Chart } from './chart'; export { default as ChartPlaceholder } from './chart/placeholder'; export { CompareButton, CompareFilter } from './compare-filter'; +export { ConditionalWrapper as __experimentalConditionalWrapper } from './conditional-wrapper'; export { default as Date } from './date'; export { default as DateRangeFilterPicker } from './date-range-filter-picker'; export { default as DateRange } from './calendar/date-range'; diff --git a/packages/js/components/src/util/index.ts b/packages/js/components/src/util/index.ts deleted file mode 100644 index ed673a8ca5b..00000000000 --- a/packages/js/components/src/util/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './conditional-wrapper'; diff --git a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss index 3558a817a3c..87bb136736c 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss @@ -21,6 +21,14 @@ } } + .woocommerce-product-form__field { + margin-top: $gap-large; + + > .components-base-control { + margin-bottom: 0; + } + } + .components-radio-control .components-v-stack { gap: $gap-small; } diff --git a/plugins/woocommerce-admin/client/products/product-page.scss b/plugins/woocommerce-admin/client/products/product-page.scss index 23c9c9ac2d8..dae149dad32 100644 --- a/plugins/woocommerce-admin/client/products/product-page.scss +++ b/plugins/woocommerce-admin/client/products/product-page.scss @@ -58,6 +58,30 @@ margin-bottom: 0; } } + + // This is needed because Gutenberg disables tooltip events on disabled elements. + // We are explicitly using this on a disabled item so this overlay prevents + // the tooltip from seeing the disabled property and allows mouse events to occur. + // See https://github.com/WordPress/gutenberg/blob/411b6eee8376e31bf9db4c15c92a80524ae38e9b/packages/components/src/tooltip/index.js#L99-L102 + .woocommerce-product-form__tooltip-disabled-overlay { + position: relative; + display: inline-block; + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 100%; + height: 100%; + background: transparent; + left: 0; + top: 0; + cursor: default; + } + + .components-base-control { + margin-bottom: 0; + } + } } .woocommerce-edit-product { diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx index cc565a19eb7..8bc52d4f300 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx @@ -2,16 +2,22 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { + __experimentalConditionalWrapper as ConditionalWrapper, + Link, + useFormContext, +} from '@woocommerce/components'; +import { cloneElement } from '@wordpress/element'; +import { getAdminLink } from '@woocommerce/settings'; +import { Product } from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; import { Card, CardBody, ToggleControl, TextControl, + Tooltip, } from '@wordpress/components'; -import { getAdminLink } from '@woocommerce/settings'; -import { Link, useFormContext } from '@woocommerce/components'; -import { Product } from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies @@ -70,8 +76,23 @@ export const ProductInventorySection: React.FC = () => { ) } { ...getInputProps( 'sku' ) } /> - { canManageStock && ( - <> +
+ ( + +
+ { children } +
+
+ ) } + > { 'manage_stock', getCheckboxTracks( 'manage_stock' ) ) } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This prop does exist, but is not typed in @wordpress/components. + disabled={ ! canManageStock } /> - { values.manage_stock && } - +
+
+ { values.manage_stock ? ( + + ) : ( + ) } - { ! values.manage_stock && }
diff --git a/plugins/woocommerce/changelog/add-35046 b/plugins/woocommerce/changelog/add-35046 new file mode 100644 index 00000000000..0276e8b8ba5 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35046 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Disable inventory stock toggle when product stock management is disabled From ed2399680835f0e29cd4526f3443ddfc5db19bfa Mon Sep 17 00:00:00 2001 From: Jamel Noel Reid Date: Mon, 24 Oct 2022 18:24:28 -0500 Subject: [PATCH 057/149] Update permalink structure using Playwright global setup (#35282) Setup permalink in global-setup.js --- .../changelog/add-setup-permalinks | 4 ++ .../woocommerce/tests/e2e-pw/global-setup.js | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-setup-permalinks diff --git a/plugins/woocommerce/changelog/add-setup-permalinks b/plugins/woocommerce/changelog/add-setup-permalinks new file mode 100644 index 00000000000..6378e6aef57 --- /dev/null +++ b/plugins/woocommerce/changelog/add-setup-permalinks @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Uses the globa-setup.js to setup permalinks structure diff --git a/plugins/woocommerce/tests/e2e-pw/global-setup.js b/plugins/woocommerce/tests/e2e-pw/global-setup.js index 988aabc36a5..f443dd56cb6 100644 --- a/plugins/woocommerce/tests/e2e-pw/global-setup.js +++ b/plugins/woocommerce/tests/e2e-pw/global-setup.js @@ -11,6 +11,19 @@ const adminPassword = ADMIN_PASSWORD ?? 'password'; const customerUsername = CUSTOMER_USER ?? 'customer'; const customerPassword = CUSTOMER_PASSWORD ?? 'password'; +const setupPermalinks = async ( adminPage, baseURL ) => { + console.log( 'Trying to setup permalinks!' ); + await adminPage.goto( baseURL + '/wp-admin/options-permalink.php' ); + await expect( adminPage.locator( 'div.wrap > h1' ) ).toHaveText( + 'Permalink Settings' + ); + await adminPage.click( '#custom_selection' ); + await adminPage.fill( '#permalink_structure', '/%postname%/' ); + await adminPage.click( '#submit' ); + + console.log( 'Permalinks Set!' ); +} + module.exports = async ( config ) => { const { stateDir, baseURL, userAgent } = config.projects[ 0 ].use; @@ -129,6 +142,30 @@ module.exports = async ( config ) => { process.exit( 1 ); } + // Ensure that permalinks are correctly setup since we can't set it up using wp-env when not using the default WP version. + // More info here: https://github.com/WordPress/gutenberg/issues/28201 + let permalinkConfigured = false; + const permalinkRetries = 5; + for ( let i = 0; i < permalinkRetries; i++ ) { + try { + await setupPermalinks( adminPage, baseURL ); + permalinkConfigured = true; + break; + } catch ( e ) { + console.log( + `Setting permalink failed, Retrying... ${ i }/${ permalinkRetries }` + ); + console.log( e ); + } + } + + if ( ! permalinkConfigured ) { + console.error( + 'Cannot proceed e2e test, as we could not setup permalinks. Please check if the test site has been setup correctly.' + ); + process.exit( 1 ); + } + // Sign in as customer user and save state const customerRetries = 5; for ( let i = 0; i < customerRetries; i++ ) { From 39b472be7af923bb5dab01a0d165b6b55c0a565f Mon Sep 17 00:00:00 2001 From: rodelgc Date: Tue, 25 Oct 2022 07:53:46 +0800 Subject: [PATCH 058/149] Set paths for `allure-results`, `test-results.json`, and save state files to be inside their respective E2E or API folders (#35206) * Set paths to e2e-pw * Delete and untrack storage state files * Add changelog * Checkout updated version of smoke test daily workflow * Allow setting allure output paths to e2e-pw folder using environment variables * Set allure output paths to be inside api-core-tests/api-test-report folder * Remove unnecessary TODO comment --- .../cot-build-and-e2e-tests-daily.yml | 36 +++++----- .../workflows/cot-pr-build-and-e2e-tests.yml | 26 ++++---- .github/workflows/pr-build-and-e2e-tests.yml | 24 ++++--- .github/workflows/smoke-test-daily.yml | 23 ++++--- .../e2e-use-e2e-pw-folder-for-outputs | 4 ++ .../tests/api-core-tests/playwright.config.js | 14 +++- .../tests/e2e-pw/playwright.config.js | 25 ++++--- .../tests/e2e-pw/storage/adminState.json | 65 ------------------- .../tests/e2e-pw/storage/customerState.json | 45 ------------- 9 files changed, 96 insertions(+), 166 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs delete mode 100644 plugins/woocommerce/tests/e2e-pw/storage/adminState.json delete mode 100644 plugins/woocommerce/tests/e2e-pw/storage/customerState.json diff --git a/.github/workflows/cot-build-and-e2e-tests-daily.yml b/.github/workflows/cot-build-and-e2e-tests-daily.yml index ccea0f0dd25..b4beda9e3fc 100644 --- a/.github/workflows/cot-build-and-e2e-tests-daily.yml +++ b/.github/workflows/cot-build-and-e2e-tests-daily.yml @@ -1,20 +1,23 @@ name: Run daily tests in an environment with COT enabled on: - schedule: - - cron: "30 2 * * *" - workflow_dispatch: + schedule: + - cron: '30 2 * * *' + workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: cot-e2e-tests-run: name: Runs E2E tests with COT enabled. runs-on: ubuntu-20.04 + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo @@ -30,7 +33,7 @@ jobs: timeout-minutes: 60 id: run_playwright_e2e_tests env: - USE_WP_ENV: 1 + USE_WP_ENV: 1 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -43,7 +46,7 @@ jobs: steps.run_playwright_e2e_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright E2E test report if: | @@ -53,8 +56,8 @@ jobs: with: name: e2e-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -62,10 +65,11 @@ jobs: name: Runs API tests with COT enabled. runs-on: ubuntu-20.04 env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo @@ -81,6 +85,7 @@ jobs: USER_KEY: admin USER_SECRET: password run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js + - name: Generate Playwright API Test report. id: generate_api_report if: | @@ -90,7 +95,8 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + - name: Archive Playwright API test report if: | always() && @@ -99,8 +105,8 @@ jobs: with: name: api-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 diff --git a/.github/workflows/cot-pr-build-and-e2e-tests.yml b/.github/workflows/cot-pr-build-and-e2e-tests.yml index 13ff7080081..1a1531deae8 100644 --- a/.github/workflows/cot-pr-build-and-e2e-tests.yml +++ b/.github/workflows/cot-pr-build-and-e2e-tests.yml @@ -13,9 +13,12 @@ jobs: name: Runs E2E tests with COT enabled. if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}" runs-on: ubuntu-20.04 + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo @@ -31,7 +34,7 @@ jobs: timeout-minutes: 60 id: run_playwright_e2e_tests env: - USE_WP_ENV: 1 + USE_WP_ENV: 1 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -44,7 +47,7 @@ jobs: steps.run_playwright_e2e_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright E2E test report if: | @@ -54,8 +57,8 @@ jobs: with: name: e2e-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -64,10 +67,11 @@ jobs: if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}" runs-on: ubuntu-20.04 env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo @@ -93,8 +97,8 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report - + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + - name: Archive Playwright API test report if: | always() && @@ -103,8 +107,8 @@ jobs: with: name: api-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml index 00757a05801..f6a4c8e3f47 100644 --- a/.github/workflows/pr-build-and-e2e-tests.yml +++ b/.github/workflows/pr-build-and-e2e-tests.yml @@ -11,6 +11,9 @@ jobs: e2e-tests-run: name: Runs E2E tests. runs-on: ubuntu-20.04 + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report outputs: E2E_GRAND_TOTAL: ${{ steps.count_e2e_total.outputs.E2E_GRAND_TOTAL }} steps: @@ -40,9 +43,9 @@ jobs: timeout-minutes: 60 id: run_playwright_e2e_tests env: - USE_WP_ENV: 1 - E2E_MAX_FAILURES: 15 - FORCE_COLOR: 1 + USE_WP_ENV: 1 + E2E_MAX_FAILURES: 15 + FORCE_COLOR: 1 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -55,7 +58,7 @@ jobs: steps.run_playwright_e2e_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright E2E test report if: | @@ -65,8 +68,8 @@ jobs: with: name: e2e-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -74,7 +77,8 @@ jobs: name: Runs API tests. runs-on: ubuntu-20.04 env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 @@ -103,7 +107,7 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright API test report if: | always() && @@ -112,8 +116,8 @@ jobs: with: name: api-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 7774735db14..640cd61cd27 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -68,7 +68,7 @@ jobs: steps.e2e.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive E2E test report if: | @@ -78,8 +78,8 @@ jobs: with: name: ${{ env.E2E_ARTIFACT }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -88,6 +88,9 @@ jobs: runs-on: ubuntu-20.04 needs: [e2e-tests] if: always() + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 with: @@ -119,7 +122,7 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive API test report if: | @@ -129,8 +132,8 @@ jobs: with: name: ${{ env.API_ARTIFACT }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -194,6 +197,8 @@ jobs: if: always() env: USE_WP_ENV: 1 + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report strategy: fail-fast: false matrix: @@ -254,7 +259,7 @@ jobs: steps.e2e.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive E2E test report if: | @@ -264,8 +269,8 @@ jobs: with: name: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }}) path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 diff --git a/plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs b/plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs new file mode 100644 index 00000000000..6ef49004835 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Use plugins/woocommerce/tests/e2e-pw folder for saving test outputs diff --git a/plugins/woocommerce/tests/api-core-tests/playwright.config.js b/plugins/woocommerce/tests/api-core-tests/playwright.config.js index caf5638cbb2..ed1ecd645b6 100644 --- a/plugins/woocommerce/tests/api-core-tests/playwright.config.js +++ b/plugins/woocommerce/tests/api-core-tests/playwright.config.js @@ -34,9 +34,19 @@ const config = { ], [ 'allure-playwright', - { outputFolder: 'api-test-report/allure-results' }, + { + outputFolder: + process.env.ALLURE_RESULTS_DIR ?? + 'tests/api-core-tests/api-test-report/allure-results', + }, + ], + [ + 'json', + { + outputFile: + 'tests/api-core-tests/api-test-report/test-results.json', + }, ], - [ 'json', { outputFile: 'api-test-report/test-results.json' } ], ], use: { screenshot: 'only-on-failure', diff --git a/plugins/woocommerce/tests/e2e-pw/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/playwright.config.js index 07db8f7bda0..f218c67baf8 100644 --- a/plugins/woocommerce/tests/e2e-pw/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/playwright.config.js @@ -1,9 +1,10 @@ const { devices } = require( '@playwright/test' ); const { - CI, - E2E_MAX_FAILURES, + ALLURE_RESULTS_DIR, BASE_URL, + CI, DEFAULT_TIMEOUT_OVERRIDE, + E2E_MAX_FAILURES, } = process.env; const config = { @@ -26,17 +27,23 @@ const config = { open: CI ? 'never' : 'always', }, ], - [ 'allure-playwright', { outputFolder: 'e2e/allure-results' } ], - [ 'json', { outputFile: 'e2e/test-results.json' } ], + [ + 'allure-playwright', + { + outputFolder: + ALLURE_RESULTS_DIR ?? 'tests/e2e-pw/allure-results', + }, + ], + [ 'json', { outputFile: 'tests/e2e-pw/test-results.json' } ], ], maxFailures: E2E_MAX_FAILURES ? Number( E2E_MAX_FAILURES ) : 0, use: { - screenshot: 'only-on-failure', - video: 'on-first-retry', - trace: 'retain-on-failure', - viewport: { width: 1280, height: 720 }, baseURL: BASE_URL ?? 'http://localhost:8086', - stateDir: 'e2e/storage/', + screenshot: 'only-on-failure', + stateDir: 'tests/e2e-pw/storage/', + trace: 'retain-on-failure', + video: 'on-first-retry', + viewport: { width: 1280, height: 720 }, }, projects: [ { diff --git a/plugins/woocommerce/tests/e2e-pw/storage/adminState.json b/plugins/woocommerce/tests/e2e-pw/storage/adminState.json deleted file mode 100644 index 8098ee78c77..00000000000 --- a/plugins/woocommerce/tests/e2e-pw/storage/adminState.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "cookies": [ - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "admin%7C1660405922%7CjsNIRwmbjFh7KIGlSHy50rSs6X0L0zISppmBqgh0u0u%7C8ac4ff905331544a3cbcf0c58840ebfc3f492d4511dd17f48dbed18f5662a2c6", - "domain": "localhost", - "path": "/wp-content/plugins", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "admin%7C1660405922%7CjsNIRwmbjFh7KIGlSHy50rSs6X0L0zISppmBqgh0u0u%7C8ac4ff905331544a3cbcf0c58840ebfc3f492d4511dd17f48dbed18f5662a2c6", - "domain": "localhost", - "path": "/wp-admin", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_test_cookie", - "value": "WP%20Cookie%20check", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_logged_in_dc5025de8b60c0a511df7c07d81ead97", - "value": "admin%7C1660405922%7CjsNIRwmbjFh7KIGlSHy50rSs6X0L0zISppmBqgh0u0u%7C6de27e0d8393de7715c07e144424d90733d53e65b8665c8a7db1ad94798c369c", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "tk_ai", - "value": "woo%3AT9QFbJmpuSGWcjApaONaBtSB", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wp-settings-time-1", - "value": "1660233126", - "domain": "localhost", - "path": "/", - "expires": 1691769126.937059, - "httpOnly": false, - "secure": false - } - ], - "origins": [] -} \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/storage/customerState.json b/plugins/woocommerce/tests/e2e-pw/storage/customerState.json deleted file mode 100644 index 6fa41f487e0..00000000000 --- a/plugins/woocommerce/tests/e2e-pw/storage/customerState.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "cookies": [ - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "customer%7C1660405929%7CL0OCLRBBMubq8iKqOeQU1NqOzTWFd7ppECd1n100GKG%7Cccb6b6b9e1190e94ef751360fded6ec026c89350f9f4695e9bf900ca09d72a84", - "domain": "localhost", - "path": "/wp-content/plugins", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "customer%7C1660405929%7CL0OCLRBBMubq8iKqOeQU1NqOzTWFd7ppECd1n100GKG%7Cccb6b6b9e1190e94ef751360fded6ec026c89350f9f4695e9bf900ca09d72a84", - "domain": "localhost", - "path": "/wp-admin", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_test_cookie", - "value": "WP%20Cookie%20check", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_logged_in_dc5025de8b60c0a511df7c07d81ead97", - "value": "customer%7C1660405929%7CL0OCLRBBMubq8iKqOeQU1NqOzTWFd7ppECd1n100GKG%7C869a28fa8ef17799b6d097d84c8155c7d0dd18cd9967f7a18e3f49f41dd71226", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": true, - "secure": false - } - ], - "origins": [] -} \ No newline at end of file From a5e3215f23079e57a76c13615192e644f570ceb8 Mon Sep 17 00:00:00 2001 From: Md Mehedi Hasan <43073560+HeyMehedi@users.noreply.github.com> Date: Tue, 25 Oct 2022 07:38:39 +0600 Subject: [PATCH 059/149] Fix: Typo Mistake (#35111) * Fix: Typo Mistake * changelog added Co-authored-by: HeyMehedi --- plugins/woocommerce/changelog/typo | 4 ++++ .../woocommerce/includes/cli/class-wc-cli-rest-command.php | 2 +- plugins/woocommerce/src/Packages.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/typo diff --git a/plugins/woocommerce/changelog/typo b/plugins/woocommerce/changelog/typo new file mode 100644 index 00000000000..01309888727 --- /dev/null +++ b/plugins/woocommerce/changelog/typo @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +typo fix diff --git a/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php index f4e22392c27..f3aac41fde2 100644 --- a/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php +++ b/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php @@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * Main Command for WooCommere CLI. + * Main Command for WooCommerce CLI. * * Since a lot of WC operations can be handled via the REST API, we base our CLI * off of Restful to generate commands for each WooCommerce REST API endpoint diff --git a/plugins/woocommerce/src/Packages.php b/plugins/woocommerce/src/Packages.php index c9948f60815..f99b3a80436 100644 --- a/plugins/woocommerce/src/Packages.php +++ b/plugins/woocommerce/src/Packages.php @@ -1,6 +1,6 @@ Date: Tue, 25 Oct 2022 20:17:38 +1300 Subject: [PATCH 060/149] Remove phpcs Github action in favor of running phpcs directly (#35237) --- .../setup-woocommerce-monorepo/action.yml | 1 + .github/workflows/pr-code-sniff.yml | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index b83f9809fb0..3ca1431d678 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -42,6 +42,7 @@ runs: with: php-version: ${{ inputs.php-version }} coverage: none + tools: cs2pr, phpcs - name: Cache Composer Dependencies uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml index 638a0b46d7f..7a8254850d8 100644 --- a/.github/workflows/pr-code-sniff.yml +++ b/.github/workflows/pr-code-sniff.yml @@ -16,20 +16,26 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + + - name: Get Changed Files + id: changed-files + uses: tj-actions/changed-files@v32 + with: + files: | + **/*.php - name: Setup WooCommerce Monorepo + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/actions/setup-woocommerce-monorepo with: build: false - name: Tool versions + if: steps.changed-files.outputs.any_changed == 'true' run: | php --version composer --version - - name: Run code sniffer - uses: thenabeel/action-phpcs@v8 - with: - files: "**.php" - phpcs_path: plugins/woocommerce/vendor/bin/phpcs - standard: phpcs.xml + - name: Run PHPCS + if: steps.changed-files.outputs.any_changed == 'true' + run: ./plugins/woocommerce/vendor/bin/phpcs -n -q --report=checkstyle ${{ steps.changed-files.outputs.all_changed_files }} | cs2pr From 43eeb0c997a34c146088c5ce7a5cab1a73ed10af Mon Sep 17 00:00:00 2001 From: Greg <71906536+zhongruige@users.noreply.github.com> Date: Tue, 25 Oct 2022 04:07:13 -0600 Subject: [PATCH 061/149] Add an NPM script to run the Playwright API Core Tests (#35283) * Added script for running Playwright API Core Tests * Added changelog --- plugins/woocommerce/changelog/add-api-pw-npm-script | 4 ++++ plugins/woocommerce/package.json | 1 + 2 files changed, 5 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-api-pw-npm-script diff --git a/plugins/woocommerce/changelog/add-api-pw-npm-script b/plugins/woocommerce/changelog/add-api-pw-npm-script new file mode 100644 index 00000000000..ad00e99c15a --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-pw-npm-script @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Added npm script for Playwright API Core Tests diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index bddcf4c5ef0..e56d993d17a 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -32,6 +32,7 @@ "env:dev": "pnpm wp-env start", "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh", "e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", + "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", "env:test:cot": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh --cot", "env:performance-init": "./tests/performance/bin/init-sample-products.sh", "env:down": "pnpm wp-env stop", From 69e52c2fa0b4d7314b7c04a8c37564ab88815c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 25 Oct 2022 10:17:43 -0300 Subject: [PATCH 062/149] Remove some placeholder values (#35267) --- .../client/products/sections/pricing-section.tsx | 2 -- .../product-inventory-section/product-inventory-section.tsx | 4 ---- .../changelog/enhancement-35174-remove-placeholders | 4 ++++ 3 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35174-remove-placeholders diff --git a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx index 6dae5ad1310..9deaaefaf3e 100644 --- a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx @@ -139,7 +139,6 @@ export const PricingSection: React.FC = () => { { { 'SKU (Stock Keeping Unit)', 'woocommerce' ) } - placeholder={ __( - 'washed-oxford-button-down-shirt', - 'woocommerce' - ) } { ...getInputProps( 'sku' ) } />
diff --git a/plugins/woocommerce/changelog/enhancement-35174-remove-placeholders b/plugins/woocommerce/changelog/enhancement-35174-remove-placeholders new file mode 100644 index 00000000000..a4ebb4853b5 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35174-remove-placeholders @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Remove some placeholder values From a7264fa6d7d9b9826f69d852eccf27757b6beceb Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Tue, 25 Oct 2022 15:23:49 +0100 Subject: [PATCH 063/149] add webhooks api-core-tests (#35292) --- .../add-api-core-tests-webhooks-crud-tests | 4 + .../tests/webhooks/webhooks-crud.test.js | 254 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests create mode 100644 plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js diff --git a/plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests new file mode 100644 index 00000000000..87cb2494912 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for webhooks crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js new file mode 100644 index 00000000000..ff5895281d8 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js @@ -0,0 +1,254 @@ +const { + test, + expect +} = require('@playwright/test'); +const exp = require('constants'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group webhooks + * + */ +test.describe('Webhooks API tests', () => { + let webhookId; + + test.describe('Create a webhook', () => { + + test('can create a webhook', async ({ + request, + }) => { + // call API to create a webhook + const response = await request.post( + '/wp-json/wc/v3/webhooks', { + data: { + name: "Order updated", + topic: "order.updated", + delivery_url: "http://requestb.in/1g0sxmo1" + }, + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(201); + expect(typeof responseJSON.id).toEqual('number'); + expect(responseJSON.name).toEqual("Order updated"); + expect(responseJSON.status).toEqual("active"); + expect(responseJSON.topic).toEqual("order.updated"); + expect(responseJSON.delivery_url).toEqual("http://requestb.in/1g0sxmo1"); + expect(responseJSON.hooks).toEqual( + expect.arrayContaining([ + "woocommerce_update_order", + "woocommerce_order_refunded" + ]) + ); + + webhookId = responseJSON.id; + }); + + }); + + test.describe('Retrieve after create', () => { + test('can retrieve a webhook', async ({ + request + }) => { + // call API to retrieve the previously saved webhook + const response = await request.get( + `/wp-json/wc/v3/webhooks/${webhookId}` + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + expect(typeof responseJSON.id).toEqual('number'); + expect(responseJSON.name).toEqual("Order updated"); + expect(responseJSON.status).toEqual("active"); + expect(responseJSON.topic).toEqual("order.updated"); + expect(responseJSON.delivery_url).toEqual("http://requestb.in/1g0sxmo1"); + expect(responseJSON.hooks).toEqual( + expect.arrayContaining([ + "woocommerce_update_order", + "woocommerce_order_refunded" + ]) + ); + }); + + test('can retrieve all webhooks', async ({ + request + }) => { + // call API to retrieve all webhooks + const response = await request.get('/wp-json/wc/v3/webhooks'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)); + expect(responseJSON.length).toBeGreaterThan(0); + }); + }); + + test.describe('Update a webhook', () => { + test(`can update a web hook`, async ({ + request, + }) => { + // update webhook + const response = await request.put( + `/wp-json/wc/v3/webhooks/${ webhookId }`, { + data: { + status: "paused" + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.status).toEqual("paused"); + }); + }); + + test.describe('Delete a webhook', () => { + test('can permanently delete a webhook', async ({ + request + }) => { + + // Delete the webhook + const response = await request.delete(`/wp-json/wc/v3/webhooks/${ webhookId }`, { + data: { + force: true + } + }); + expect(response.status()).toEqual(200); + + // Verify that the webhook can no longer be retrieved + const getDeletedWebhookResponse = await request.get( + `/wp-json/wc/v3/webhooks/${ webhookId }` + ); + + /** + * Issue raised as we would expect this to return a 400 to be + * consistent with the other API calls + * Issue: https://github.com/woocommerce/woocommerce/issues/35290 + */ + expect(getDeletedWebhookResponse.status()).toEqual(400); + }); + }); + + + test.describe('Batch webhook operations', () => { + let webhookId1; + let webhookId2; + let webhookId3; + test('can batch create webhooks', async ({ + request + }) => { + // Batch create webhooks + // call API to batch create a webhook + const response = await request.post('wp-json/wc/v3/webhooks/batch', { + data: { + create: [{ + name: "Round toe", + topic: "coupon.created", + delivery_url: "http://requestb.in/1g0sxmo1" + }, + { + name: "Customer deleted", + topic: "customer.deleted", + delivery_url: "http://requestb.in/1g0sxmo1" + } + ] + }, + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + // Verify that the new webhooks were created + const webhooks = responseJSON.create; + expect(webhooks).toHaveLength(2); + webhookId1 = webhooks[0].id; + webhookId2 = webhooks[1].id; + expect(webhookId1).toBeDefined(); + expect(webhookId2).toBeDefined(); + expect(webhooks[0].name).toEqual('Round toe'); + expect(webhooks[1].name).toEqual('Customer deleted'); + }); + + test('can batch update webhooks', async ({ + request + }) => { + // set payload to create, update and delete webhooks + const batchUpdatePayload = { + create: [{ + name: "Order Created", + topic: "order.created", + delivery_url: "http://requestb.in/1g0sxmo1" + }, ], + update: [{ + id: webhookId1, + name: 'Square toe', + }, ], + delete: [webhookId2] + }; + + // Call API to batch update the webhooks + const response = await request.post( + 'wp-json/wc/v3/webhooks/batch', { + data: batchUpdatePayload, + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.create).toHaveLength(1); + + webhookId3 = responseJSON.create[0].id; + expect(webhookId3).toBeDefined(); + expect(responseJSON.create[0].name).toEqual('Order Created'); + expect(responseJSON.create[0].topic).toEqual('order.created'); + expect(responseJSON.create[0].delivery_url).toEqual('http://requestb.in/1g0sxmo1'); + + expect(responseJSON.update).toHaveLength(1); + expect(responseJSON.update[0].id).toEqual(webhookId1); + expect(responseJSON.update[0].name).toEqual('Square toe'); + + // Verify that the deleted webhook can no longer be retrieved + const getDeletedWebhookResponse = await request.get( + `/wp-json/wc/v3/webhooks/${ webhookId2 }` + ); + /** + * Issue raised as we would expect this to return a 400 to be + * consistent with the other API calls + * Issue: https://github.com/woocommerce/woocommerce/issues/35290 + */ + expect(getDeletedWebhookResponse.status()).toEqual(400); + + }); + + test('can batch delete webhooks', async ({ + request + }) => { + // Batch delete the created webhooks + const response = await request.post( + 'wp-json/wc/v3/webhooks/batch', { + data: { + delete: [webhookId1, webhookId3] + }, + } + ); + const responseJSON = await response.json(); + + //Call the API to attempte to retrieve the deleted webhooks + const deletedResponse1 = await request.get( + `wp-json/wc/v3/webhooks/${ webhookId1 }` + ); + const deletedResponse3 = await request.get( + `wp-json/wc/v3/webhooks/${ webhookId3 }` + ); + /** + * Issue raised as we would expect this to return a 400 to be + * consistent with the other API calls + * Issue: https://github.com/woocommerce/woocommerce/issues/35290 + */ + expect(deletedResponse1.status()).toEqual(400); + expect(deletedResponse3.status()).toEqual(400); + + }); + }); +}); From ec40d85de91aed1ec5a2ff9df85e490a8a29d2e8 Mon Sep 17 00:00:00 2001 From: Greg <71906536+zhongruige@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:42:39 -0600 Subject: [PATCH 064/149] Update Playwright API Core Tests readme (#35303) * Update api core tests readme * Added changelog * Minor spacing tweak --- .../changelog/update-pw-api-tests-readme | 4 ++ .../tests/api-core-tests/README.md | 54 ++++++++----------- 2 files changed, 26 insertions(+), 32 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-pw-api-tests-readme diff --git a/plugins/woocommerce/changelog/update-pw-api-tests-readme b/plugins/woocommerce/changelog/update-pw-api-tests-readme new file mode 100644 index 00000000000..79cdb05228e --- /dev/null +++ b/plugins/woocommerce/changelog/update-pw-api-tests-readme @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update api-core-tests readme for consistency with new command and updates to other commands too. diff --git a/plugins/woocommerce/tests/api-core-tests/README.md b/plugins/woocommerce/tests/api-core-tests/README.md index e39b68593e8..6c19aef1464 100644 --- a/plugins/woocommerce/tests/api-core-tests/README.md +++ b/plugins/woocommerce/tests/api-core-tests/README.md @@ -1,6 +1,6 @@ -# WooCommerce Playwright End to End Tests +# WooCommerce Core API Test Suite -This is the documentation for the new api-core-tests setup based on Playwright and wp-env. It superseedes the Puppeteer and e2e-environment [setup](../tests/e2e), which we will gradually deprecate. +This package contains automated API tests for WooCommerce, based on Playwright and `wp-env`. It supersedes the SuperTest based [api-core-tests package](https://www.npmjs.com/package/@woocommerce/api-core-tests) and e2e-environment [setup](../tests/e2e), which we will gradually deprecate. ## Table of contents @@ -21,36 +21,33 @@ This is the documentation for the new api-core-tests setup based on Playwright a - PNPM ([Installation instructions](https://pnpm.io/installation)) - Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/)) -Note, that if you are on Mac and you install docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary. +Note, that if you are on Mac and you install Docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary. -If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for running E2E tests. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below. +If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for running tests. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below. ### Introduction -api-core-tests are powered by Playwright. The test site is spinned up using `wp-env` (recommended), but we will continue to support `e2e-environment` in the meantime. +WooCommerce's `api-core-tests` are powered by Playwright. The test site is spun up using `wp-env` (recommended), but we will continue to support `e2e-environment` in the meantime. **Running tests for the first time:** -Note: the commands may need to be executed in `plugins/woocommerce` (or a subdirectory thereof) - `nvm use` - `pnpm install` - `pnpm run build --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` - -Then execute the tests with the following command: - -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js` (headless) +- `cd plugins/woocommerce` +- `pnpm env:test` +- `pnpm test:api-pw` To run the test again, re-create the environment to start with a fresh state -- `pnpm env:destroy --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` +- `pnpm env:destroy` +- `pnpm env:test` +- `pnpm test:api-pw` Other ways of running tests: -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js --headed` (headed) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js --debug` (debug) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js ./tests/api-core-tests/tests/hello/hello.test.js` (running a single test) +- `pnpm test:api-pw --debug` (debug) +- `pnpm test:api-pw ./tests/api-core-tests/tests/hello/hello.test.js` (running a single test) To see all options, run `cd plugins/woocommerce && pnpm playwright test --help` @@ -69,8 +66,7 @@ USER_KEY="" USER_SECRET="" ``` -For local setup, create a `.env` file in the `woocommerce/plugins/woocommerce` folder with the three required values described above. -If any of these variables are configured they will override the values automatically set in the `playwright.config.js` +For local setup, create a `.env` file in the `woocommerce/plugins/woocommerce` folder with the three required values described above. If any of these variables are configured they will override the values automatically set in the `playwright.config.js` When using a username and password combination instead of a consumer secret and consumer key, make sure to have the [JSON Basic Authentication plugin](https://github.com/WP-API/Basic-Auth) installed and activated on the test site. @@ -113,11 +109,11 @@ The test environment uses the following test variables: If you need to modify the port for your local test environment (eg. port is already in use) or use, edit [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). Depending on what environment tool you are using, you will need to also edit the respective `.json` file. -**Modiify the port wp-env** +**Modify the port wp-env** Edit [.wp-env.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/.wp-env.json) and [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). -**Modiify port for e2e-environment** +**Modify port for e2e-environment** Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).**** @@ -125,9 +121,9 @@ Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/ After you run a test, it's best to restart the environment to start from a fresh state. We are currently working to reset the state more efficiently to avoid the restart being needed, but this is a work-in-progress. -- `pnpm env:down --filter=woocommerce` to stop the environment -- `pnpm env:destroy --filter=woocommerce` when you make changes to `.wp-env.json` -- `pnpm env:test --filter=woocommerce` to spin up the test environment +- `pnpm env:down` to stop the environment +- `pnpm env:destroy` when you make changes to `.wp-env.json` +- `pnpm env:test` to spin up the test environment ## Guide for writing tests @@ -142,17 +138,11 @@ Based on our example, the test skeleton would look as follows: ```js test.describe( 'Merchant can create virtual product', () => { - test( 'merchant can log in', async () => { + test( 'merchant can log in', async () => { } ); - } ); + test( 'merchant can create virtual product', async () => { } ); - test( 'merchant can create virtual product', async () => { - - } ); - - test( 'merchant can verify that virtual product was created', async () => { - - } ); + test( 'merchant can verify that virtual product was created', async () => { } ); } ); ``` From 4cde06e151c40a4aa9ab4f82f082f7aa62cb8505 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 25 Oct 2022 19:51:34 +0200 Subject: [PATCH 065/149] Update states.php -> added Senegal Regions (#35199) * Update states.php -> added Senegal Regions * Update to CLDR. * Changelog. Co-authored-by: Papa Amadou Korka Sow <32936698+Sowgenius@users.noreply.github.com> --- plugins/woocommerce/changelog/fix-29844-replay | 4 ++++ plugins/woocommerce/i18n/states.php | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-29844-replay diff --git a/plugins/woocommerce/changelog/fix-29844-replay b/plugins/woocommerce/changelog/fix-29844-replay new file mode 100644 index 00000000000..c217cff0d73 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-29844-replay @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Added states for Senegal. diff --git a/plugins/woocommerce/i18n/states.php b/plugins/woocommerce/i18n/states.php index 9b14647d69d..dfe5a790fed 100644 --- a/plugins/woocommerce/i18n/states.php +++ b/plugins/woocommerce/i18n/states.php @@ -1543,6 +1543,22 @@ return array( 'VS' => __( 'Vaslui', 'woocommerce' ), 'VN' => __( 'Vrancea', 'woocommerce' ), ), + 'SN' => array( // Regions of Senegal. Ref: https://github.com/unicode-org/cldr/blob/release-42/common/subdivisions/en.xml#L4801. + 'SNDB' => __( 'Diourbel', 'woocommerce' ), + 'SNDK' => __( 'Dakar', 'woocommerce' ), + 'SNFK' => __( 'Fatick', 'woocommerce' ), + 'SNKA' => __( 'Kaffrine', 'woocommerce' ), + 'SNKD' => __( 'Kolda', 'woocommerce' ), + 'SNKE' => __( 'Kédougou', 'woocommerce' ), + 'SNKL' => __( 'Kaolack', 'woocommerce' ), + 'SNLG' => __( 'Louga', 'woocommerce' ), + 'SNMT' => __( 'Matam', 'woocommerce' ), + 'SNSE' => __( 'Sédhiou', 'woocommerce' ), + 'SNSL' => __( 'Saint-Louis', 'woocommerce' ), + 'SNTC' => __( 'Tambacounda', 'woocommerce' ), + 'SNTH' => __( 'Thiès', 'woocommerce' ), + 'SNZG' => __( 'Ziguinchor', 'woocommerce' ), + ), 'SG' => array(), 'SK' => array(), 'SI' => array(), From 9ca5cbe3a54c8ab75e5430491ee83c71c4e4b32e Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 25 Oct 2022 14:56:29 -0400 Subject: [PATCH 066/149] Fix DateTimePickerControl suffix style (#35256) --- .../changelog/fix-date-time-picker-control-suffix-style | 5 +++++ .../date-time-picker-control/date-time-picker-control.scss | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 packages/js/components/changelog/fix-date-time-picker-control-suffix-style diff --git a/packages/js/components/changelog/fix-date-time-picker-control-suffix-style b/packages/js/components/changelog/fix-date-time-picker-control-suffix-style new file mode 100644 index 00000000000..97b797ed6cb --- /dev/null +++ b/packages/js/components/changelog/fix-date-time-picker-control-suffix-style @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Just a minor tweak to the CSS for the DateTimePickerControl suffix. + + diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss index feb98a25427..4930b8c7cf8 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss @@ -1,8 +1,8 @@ .woocommerce-date-time-picker-control { display: block; - .woocommerce-date-time-picker-control__input-control__suffix { - padding-right: 8px; + .components-input-control__suffix { + margin-right: 8px; } .components-datetime__date { From 1bca35c36011944c2048cab1a7690ab52b7c89e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 25 Oct 2022 16:49:02 -0300 Subject: [PATCH 067/149] Improve the communication around required and optional (#35266) * Improve the communication around required and optional * Add comments suggestions --- .../products/sections/pricing-section.tsx | 12 +------ .../sections/product-details-section.tsx | 17 ++++++++- .../test/product-details-section.spec.tsx | 4 ++- .../test/product-shipping-section.spec.tsx | 4 +-- .../add-new-shipping-class-modal.tsx | 35 +++++++------------ .../enhancement-35191-improve-req-opt-labels | 4 +++ 6 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels diff --git a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx index 9deaaefaf3e..d5f530eb2f8 100644 --- a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx @@ -79,16 +79,6 @@ export const PricingSection: React.FC = () => { }, } ); - const salePriceTitle = interpolateComponents( { - mixedString: __( - 'Sale price {{span}}(optional){{/span}}', - 'woocommerce' - ), - components: { - span: , - }, - } ); - const currencyInputProps = { ...getCurrencySymbolProps( currencyConfig ), sanitize: ( value: Product[ keyof Product ] ) => { @@ -159,7 +149,7 @@ export const PricingSection: React.FC = () => { > {
+ { __( + '(required)', + 'woocommerce' + ) } + + ), + }, + } ) } name={ `${ PRODUCT_DETAILS_SLUG }-name` } placeholder={ __( 'e.g. 12 oz Coffee Mug', diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx index 5ce9b393cd3..fed5d6a532a 100644 --- a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx +++ b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx @@ -78,7 +78,9 @@ describe( 'ProductDetailsSection', () => { ); - userEvent.clear( screen.getByLabelText( 'Name' ) ); + userEvent.clear( + screen.getByLabelText( 'Name', { exact: false } ) + ); userEvent.tab(); expect( screen.queryByText( linkUrl ) ).not.toBeInTheDocument(); diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx index 235853ac4d8..9818765b7c0 100644 --- a/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx +++ b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx @@ -32,12 +32,12 @@ function getShippingClassSelect() { async function getShippingClassNameInput() { const dialog = getShippingClassDialog(); - return within( dialog ).getByLabelText( __( 'Name', 'woocommerce' ) ); + return within( dialog ).getByLabelText( 'Name', { exact: false } ); } async function getShippingClassSlugInput() { const dialog = getShippingClassDialog(); - return within( dialog ).getByLabelText( 'Slug', { exact: false } ); + return within( dialog ).getByLabelText( __( 'Slug', 'woocommerce' ) ); } async function openShippingClassDialog() { diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx index 21962919ec2..b4b12e3d859 100644 --- a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx @@ -39,36 +39,25 @@ function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) {
+ { __( '(required)', 'woocommerce' ) } + + ), + }, + } ) } /> - ), - }, - } ) } + label={ __( 'Slug', 'woocommerce' ) } /> - ), - }, - } ) } + label={ __( 'Description', 'woocommerce' ) } help={ errors?.description ?? __( diff --git a/plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels b/plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels new file mode 100644 index 00000000000..81e2302df00 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Improve the communication around required and optional From c1e5098ff2f149382277b9047f65ead3b3af7f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 25 Oct 2022 16:49:48 -0300 Subject: [PATCH 068/149] Change the product info section title to Product Details (#35255) * Change the product info section title to Product Details * Add comment suggestions --- .../client/products/sections/product-details-section.tsx | 2 +- plugins/woocommerce/changelog/enhancement-35171-product-title | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35171-product-title diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index f181976ac0d..bf56bca4973 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -80,7 +80,7 @@ export const ProductDetailsSection: React.FC = () => { return ( Date: Tue, 25 Oct 2022 15:50:59 -0400 Subject: [PATCH 069/149] Support PHP style date format specifiers in DateTimePickerControl (#35285) * Use @wordpress/date to format for display in DateTimePickerControl * Update default formats to PHP style * Update tests to use @wordpress/date formatting * Update CustomDateTimeFormat story to use PHP style formatting --- ...update-date-time-picker-control-formatting | 4 ++ .../date-time-picker-control.tsx | 18 +++++---- .../stories/index.tsx | 6 ++- .../date-time-picker-control/test/index.tsx | 37 ++++++++++--------- 4 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 packages/js/components/changelog/update-date-time-picker-control-formatting diff --git a/packages/js/components/changelog/update-date-time-picker-control-formatting b/packages/js/components/changelog/update-date-time-picker-control-formatting new file mode 100644 index 00000000000..800e0dbc01d --- /dev/null +++ b/packages/js/components/changelog/update-date-time-picker-control-formatting @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Switch DateTimePickerControl formatting to PHP style, for WP compatibility. diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index 69c40f7464b..57626549bf1 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import { format as formatDate } from '@wordpress/date'; import { createElement, useState, @@ -22,9 +23,11 @@ import { __experimentalInputControl as InputControl, } from '@wordpress/components'; -export const defaultDateFormat = 'MM/DD/YYYY'; -export const default12HourDateTimeFormat = 'MM/DD/YYYY h:mm a'; -export const default24HourDateTimeFormat = 'MM/DD/YYYY H:mm'; +// PHP style formatting: +// https://wordpress.org/support/article/formatting-date-and-time/ +export const defaultDateFormat = 'm/d/Y'; +export const default12HourDateTimeFormat = 'm/d/Y h:i a'; +export const default24HourDateTimeFormat = 'm/d/Y H:i'; export type DateTimePickerControlOnChangeHandler = ( date: string, @@ -103,9 +106,10 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { return moment.utc( dateString, moment.ISO_8601, true ); } - function parseMoment( dateString?: string | null ): Moment { - // parse input date string as local time - return moment( dateString, displayFormat ); + function parseMoment( dateString: string | null ): Moment { + // parse input date string as local time; + // be lenient of user input and try to match any format Moment can + return moment( dateString ); } function formatMomentIso( momentDate: Moment ): string { @@ -113,7 +117,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } function formatMoment( momentDate: Moment ): string { - return momentDate.local().format( displayFormat ); + return formatDate( displayFormat, momentDate.local() ); } function hasFocusLeftInputAndDropdownContent( diff --git a/packages/js/components/src/date-time-picker-control/stories/index.tsx b/packages/js/components/src/date-time-picker-control/stories/index.tsx index 2fd1d3fea21..2e37931d9df 100644 --- a/packages/js/components/src/date-time-picker-control/stories/index.tsx +++ b/packages/js/components/src/date-time-picker-control/stories/index.tsx @@ -28,11 +28,13 @@ Basic.args = { help: 'Type a date and time or use the picker', }; +const customFormat = 'Y-m-d H:i'; + export const CustomDateTimeFormat = Template.bind( {} ); CustomDateTimeFormat.args = { ...Basic.args, - help: 'Format: YYYY-MM-DD HH:mm', - dateTimeFormat: 'YYYY-MM-DD HH:mm', + help: 'Format: ' + customFormat, + dateTimeFormat: customFormat, }; function ControlledContainer( { children, ...props } ) { diff --git a/packages/js/components/src/date-time-picker-control/test/index.tsx b/packages/js/components/src/date-time-picker-control/test/index.tsx index 2750fa4d55d..c5fbcd9090f 100644 --- a/packages/js/components/src/date-time-picker-control/test/index.tsx +++ b/packages/js/components/src/date-time-picker-control/test/index.tsx @@ -3,6 +3,7 @@ */ import { render, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { format as formatDate } from '@wordpress/date'; import { createElement, Fragment } from '@wordpress/element'; import moment from 'moment'; @@ -102,7 +103,7 @@ describe( 'DateTimePickerControl', () => { const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - dateTime.format( default24HourDateTimeFormat ) + formatDate( default24HourDateTimeFormat, dateTime ) ); } ); @@ -119,10 +120,10 @@ describe( 'DateTimePickerControl', () => { const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - moment - .utc( ambiguousISODateTimeString ) - .local() - .format( default24HourDateTimeFormat ) + formatDate( + default24HourDateTimeFormat, + moment.utc( ambiguousISODateTimeString ).local() + ) ); } ); @@ -139,10 +140,10 @@ describe( 'DateTimePickerControl', () => { const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - moment - .utc( unambiguousISODateTimeString ) - .local() - .format( default24HourDateTimeFormat ) + formatDate( + default24HourDateTimeFormat, + moment.utc( unambiguousISODateTimeString ).local() + ) ); } ); @@ -158,7 +159,7 @@ describe( 'DateTimePickerControl', () => { const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - dateTime.format( default12HourDateTimeFormat ) + formatDate( default12HourDateTimeFormat, dateTime ) ); } ); @@ -174,7 +175,7 @@ describe( 'DateTimePickerControl', () => { ); const input = container.querySelector( 'input' ); - expect( input?.value ).toBe( dateTime.format( dateTimeFormat ) ); + expect( input?.value ).toBe( formatDate( dateTimeFormat, dateTime ) ); } ); it( 'should update the input when currentDate is changed', () => { @@ -197,7 +198,7 @@ describe( 'DateTimePickerControl', () => { const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - updatedDateTime.format( default24HourDateTimeFormat ) + formatDate( default24HourDateTimeFormat, updatedDateTime ) ); } ); @@ -305,9 +306,9 @@ describe( 'DateTimePickerControl', () => { // TypeError: Cannot read properties of null (reading 'createEvent') it( 'should call onChange when the input is changed', async () => { const originalDateTime = moment( '2022-09-15 02:30:40' ); - const dateTimeFormat = 'HH:mm, MM-DD-YYYY'; - const newDateTimeInputString = '02:04, 06-08-2010'; - const newDateTime = moment( newDateTimeInputString, dateTimeFormat ); + const dateTimeFormat = 'm-d-Y, H:i'; + const newDateTimeInputString = '06-08-2010, 02:04'; + const newDateTime = moment( newDateTimeInputString ); const onChangeHandler = jest.fn(); const { container } = render( @@ -377,9 +378,9 @@ describe( 'DateTimePickerControl', () => { // TypeError: Cannot read properties of null (reading 'createEvent') it( 'should call the current onChange when the input is changed', async () => { const originalDateTime = moment( '2022-09-15 02:30:40' ); - const dateTimeFormat = 'HH:mm, MM-DD-YYYY'; - const newDateTimeInputString = '02:04, 06-08-2010'; - const newDateTime = moment( newDateTimeInputString, dateTimeFormat ); + const dateTimeFormat = 'm-d-Y, H:i'; + const newDateTimeInputString = '06-08-2010, 02:04'; + const newDateTime = moment( newDateTimeInputString ); const originalOnChangeHandler = jest.fn(); const newOnChangeHandler = jest.fn(); From 09df03159387d3e276c4d808b6184e52c5650845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 25 Oct 2022 17:00:11 -0300 Subject: [PATCH 070/149] Update font size and spacing in the tooltip component (#35265) --- .../changelog/enhancement-35190-update-tooltip-styles | 4 ++++ packages/js/components/src/tooltip/style.scss | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 packages/js/components/changelog/enhancement-35190-update-tooltip-styles diff --git a/packages/js/components/changelog/enhancement-35190-update-tooltip-styles b/packages/js/components/changelog/enhancement-35190-update-tooltip-styles new file mode 100644 index 00000000000..4d21861d602 --- /dev/null +++ b/packages/js/components/changelog/enhancement-35190-update-tooltip-styles @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Update font size and spacing in the tooltip component diff --git a/packages/js/components/src/tooltip/style.scss b/packages/js/components/src/tooltip/style.scss index cba998715b9..1d94d5fb767 100644 --- a/packages/js/components/src/tooltip/style.scss +++ b/packages/js/components/src/tooltip/style.scss @@ -6,7 +6,8 @@ } &__text .components-popover__content { - padding: $gap-smaller; + font-size: $default-font-size; + padding: $gap; width: max-content; } } From 78f659b702eb2b15ff2ed14d2121ca8f5916a84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 25 Oct 2022 17:17:30 -0300 Subject: [PATCH 071/149] Increase the spacing between the shipping box illustration and the dimensions fields (#35259) --- .../products/sections/product-shipping-section.scss | 11 +++++------ .../changelog/enhancement-35189-increase-gap | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35189-increase-gap diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss index 08c59941265..58261f87ab1 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss @@ -15,7 +15,7 @@ &__dimensions { &-body { display: grid; - gap: $gap-large; + gap: $gap-largest; grid-template-columns: 1fr; @media ( min-width: #{ ($break-medium) } ) { grid-template-columns: 1fr 1fr; @@ -28,15 +28,14 @@ } } } + &-image { + width: 100%; + height: 100%; + } .product-shipping-section__spinner-wrapper { @media ( min-width: #{ ($break-medium) } ) { min-height: 326px; } } } - &-image { - @media ( min-width: #{ ($break-medium) } ) { - width: 100%; - } - } } diff --git a/plugins/woocommerce/changelog/enhancement-35189-increase-gap b/plugins/woocommerce/changelog/enhancement-35189-increase-gap new file mode 100644 index 00000000000..95d6b3736f0 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35189-increase-gap @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Increase the spacing between the shipping box illustration and the dimensions fields From 47439ec24155f0d00e151fc24c011ec892c115ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Tue, 25 Oct 2022 17:28:37 -0300 Subject: [PATCH 072/149] Fix the display of letter descenders in the shipping class dropdown menu (#35258) * Fix the display of letter descenders in the shipping class dropdown menu * Add comment suggestions --- .../woocommerce-admin/client/stylesheets/shared/_global.scss | 4 ++-- .../changelog/enhancement-35188-fix-display-letters | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35188-fix-display-letters diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss index 09265e4c1a6..887d6dc4656 100644 --- a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss +++ b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss @@ -34,9 +34,9 @@ color: $gray-900; } - select.components-select-control__input { + .components-base-control select.components-select-control__input { max-width: 100%; - line-height: 1; + line-height: normal; } .components-panel__body > .components-panel__body-title, diff --git a/plugins/woocommerce/changelog/enhancement-35188-fix-display-letters b/plugins/woocommerce/changelog/enhancement-35188-fix-display-letters new file mode 100644 index 00000000000..ed7c1c39b94 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35188-fix-display-letters @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Fix the display of letter descenders in the shipping class dropdown menu From 1807fb20a48fdf06d904a6223695dde381a45312 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Tue, 25 Oct 2022 16:36:46 -0500 Subject: [PATCH 073/149] Fix reference to HEAD to work with community PRs (#35315) --- .github/workflows/pr-highlight-changes.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml index df85cdcec51..c6a0186279e 100644 --- a/.github/workflows/pr-highlight-changes.yml +++ b/.github/workflows/pr-highlight-changes.yml @@ -18,8 +18,9 @@ jobs: id: run working-directory: tools/code-analyzer run: | - version=$(pnpm run analyzer major-minor "${{ github.head_ref || github.ref_name }}" "plugins/woocommerce/woocommerce.php" | tail -n 1) - pnpm run analyzer "$GITHUB_HEAD_REF" $version -o "github" + HEAD_REF=$(git rev-parse HEAD) + version=$(pnpm run analyzer major-minor "$HEAD_REF" "plugins/woocommerce/woocommerce.php" | tail -n 1) + pnpm run analyzer "$HEAD_REF" $version -o "github" - name: Print results id: results run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}${{ steps.run.outputs.schema }}${{ steps.run.outputs.database }}" From 1e94d3fc180783fd17ae096e84b87dda21dfc5e4 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 26 Oct 2022 11:38:01 +0800 Subject: [PATCH 074/149] Fix changelog command in PR template (#35326) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a264136cb71..7114392b602 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -34,7 +34,7 @@ Closes # . - [ ] Have you added an explanation of what your changes do and why you'd like us to include them? - [ ] Have you written new tests for your changes, as applicable? -- [ ] Have you created a changelog file for each project being changed, ie `pnpm changelog add --filter=`? +- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter= changelog add`? From c4564372f6cef77492b854dcbf07d378ebdfd8d9 Mon Sep 17 00:00:00 2001 From: Alexandre Faustino Date: Wed, 26 Oct 2022 09:10:56 +0100 Subject: [PATCH 075/149] Uniform order customer search and display code (#35244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Néstor Soriano --- .../uniform-user-customer-search-and-display | 4 +++ .../class-wc-meta-box-order-data.php | 33 +++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 plugins/woocommerce/changelog/uniform-user-customer-search-and-display diff --git a/plugins/woocommerce/changelog/uniform-user-customer-search-and-display b/plugins/woocommerce/changelog/uniform-user-customer-search-and-display new file mode 100644 index 00000000000..76817ba3651 --- /dev/null +++ b/plugins/woocommerce/changelog/uniform-user-customer-search-and-display @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Make the user search metabox for orders show the same information for the loaded user and for search results diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php index dfd667d095e..3811b041491 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php @@ -223,7 +223,6 @@ class WC_Meta_Box_Order_Data { ); } - $ip_address = $order->get_customer_ip_address(); if ( $ip_address ) { $meta_list[] = sprintf( @@ -304,24 +303,32 @@ class WC_Meta_Box_Order_Data { $user_string = ''; $user_id = ''; if ( $order->get_user_id() ) { - $user_id = absint( $order->get_user_id() ); - $user = get_user_by( 'id', $user_id ); + $user_id = absint( $order->get_user_id() ); + $customer = new WC_Customer( $user_id ); + /* translators: 1: user display name 2: user ID 3: user email */ $user_string = sprintf( - /* translators: 1: user display name 2: user ID 3: user email */ + /* translators: 1: customer name, 2 customer id, 3: customer email */ esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), - $user->display_name, - absint( $user->ID ), - $user->user_email + $customer->get_first_name() . ' ' . $customer->get_last_name(), + $customer->get_id(), + $customer->get_email() ); } ?>

From c6c2828f606f18dcc9eb4dde29e9ab3015688c96 Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Wed, 26 Oct 2022 13:42:06 +0530 Subject: [PATCH 076/149] Fixed "Unsupported operand types" error. (#34327) --- plugins/woocommerce/changelog/fix-34224 | 4 ++++ plugins/woocommerce/includes/class-wc-cart.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-34224 diff --git a/plugins/woocommerce/changelog/fix-34224 b/plugins/woocommerce/changelog/fix-34224 new file mode 100644 index 00000000000..2eabccdcbd0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34224 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed "Unsupported operand types" error. diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php index 37c3810eea6..0b21b065092 100644 --- a/plugins/woocommerce/includes/class-wc-cart.php +++ b/plugins/woocommerce/includes/class-wc-cart.php @@ -2029,7 +2029,7 @@ class WC_Cart extends WC_Legacy_Cart { } } } else { - $row_price = $price * $quantity; + $row_price = (float) $price * (float) $quantity; $product_subtotal = wc_price( $row_price ); } From 332204723b854309c5c81e1578d639c132595016 Mon Sep 17 00:00:00 2001 From: Ilja Zaglov Date: Wed, 26 Oct 2022 10:12:42 +0200 Subject: [PATCH 077/149] Cat dashboard loading time (#34292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Néstor Soriano --- .../changelog/improve-cat-dashboard-loading-time | 4 ++++ .../includes/admin/class-wc-admin-dashboard-setup.php | 2 +- .../admin/class-wc-admin-dashboard-setup-test.php | 9 +++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/improve-cat-dashboard-loading-time diff --git a/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time b/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time new file mode 100644 index 00000000000..f0aed0679ee --- /dev/null +++ b/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Improve the loading time of WooCommerce setup widget for large databases diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php index f6023c2f35b..dcc2e7e8422 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php @@ -187,7 +187,7 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) : return false; } - if ( ! $this->get_task_list() || $this->get_task_list()->is_complete() || $this->get_task_list()->is_hidden() ) { + if ( ! $this->get_task_list() || $this->get_task_list()->is_hidden() || $this->get_task_list()->is_complete() ) { return false; } diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php index 5a271fda33e..1ef4c8c4ae6 100644 --- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php +++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php @@ -19,7 +19,7 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case { // Set default country to non-US so that 'payments' task gets added but 'woocommerce-payments' doesn't, // by default it won't be considered completed but we can manually change that as needed. update_option( 'woocommerce_default_country', 'JP' ); - $password = wp_generate_password( 8, false, false ); + $password = wp_generate_password( 8, false, false ); $this->admin = wp_insert_user( array( 'user_login' => "test_admin$password", @@ -88,11 +88,16 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case { * Tests widget does not display when task list is complete. */ public function test_widget_does_not_display_when_task_list_complete() { - $task_list = new class { + // phpcs:disable Squiz.Commenting + $task_list = new class() { public function is_complete() { return true; } + public function is_hidden() { + return false; + } }; + // phpcs:enable Squiz.Commenting $widget = $this->get_widget(); $widget->set_task_list( $task_list ); From 3d15a401b1b5f0e1af8979c956d4f78f3bed48a7 Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Wed, 26 Oct 2022 15:24:57 +0100 Subject: [PATCH 078/149] Add/a2 p create settings crud api core tests (#35253) * api-core-tests settings tests * api-core-tests settings tests * api-core-tests settings tests * update settings test to ensure concurrent tests do not fail --- .../add-api-core-tests-settings-crud-tests | 4 + .../tests/api-core-tests/data/settings.js | 2519 ++++++++++++++++ .../tests/settings/settings-crud.test.js | 2574 +++++++++++++++++ 3 files changed, 5097 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests create mode 100644 plugins/woocommerce/tests/api-core-tests/data/settings.js create mode 100644 plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js diff --git a/plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests new file mode 100644 index 00000000000..e8bacdc4e91 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for settingss crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/data/settings.js b/plugins/woocommerce/tests/api-core-tests/data/settings.js new file mode 100644 index 00000000000..925eddac81b --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/data/settings.js @@ -0,0 +1,2519 @@ +/** + * Default shipping zone object. + * + * For more details on shipping zone properties, see: + * + * https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-zone-properties + * + */ + +const currencies = { + "AED": "United Arab Emirates dirham (د.إ)", + "AFN": "Afghan afghani (؋)", + "ALL": "Albanian lek (L)", + "AMD": "Armenian dram (AMD)", + "ANG": "Netherlands Antillean guilder (ƒ)", + "AOA": "Angolan kwanza (Kz)", + "ARS": "Argentine peso ($)", + "AUD": "Australian dollar ($)", + "AWG": "Aruban florin (Afl.)", + "AZN": "Azerbaijani manat (AZN)", + "BAM": "Bosnia and Herzegovina convertible mark (KM)", + "BBD": "Barbadian dollar ($)", + "BDT": "Bangladeshi taka (৳ )", + "BGN": "Bulgarian lev (лв.)", + "BHD": "Bahraini dinar (.د.ب)", + "BIF": "Burundian franc (Fr)", + "BMD": "Bermudian dollar ($)", + "BND": "Brunei dollar ($)", + "BOB": "Bolivian boliviano (Bs.)", + "BRL": "Brazilian real (R$)", + "BSD": "Bahamian dollar ($)", + "BTC": "Bitcoin (฿)", + "BTN": "Bhutanese ngultrum (Nu.)", + "BWP": "Botswana pula (P)", + "BYR": "Belarusian ruble (old) (Br)", + "BYN": "Belarusian ruble (Br)", + "BZD": "Belize dollar ($)", + "CAD": "Canadian dollar ($)", + "CDF": "Congolese franc (Fr)", + "CHF": "Swiss franc (CHF)", + "CLP": "Chilean peso ($)", + "CNY": "Chinese yuan (¥)", + "COP": "Colombian peso ($)", + "CRC": "Costa Rican colón (₡)", + "CUC": "Cuban convertible peso ($)", + "CUP": "Cuban peso ($)", + "CVE": "Cape Verdean escudo ($)", + "CZK": "Czech koruna (Kč)", + "DJF": "Djiboutian franc (Fr)", + "DKK": "Danish krone (kr.)", + "DOP": "Dominican peso (RD$)", + "DZD": "Algerian dinar (د.ج)", + "EGP": "Egyptian pound (EGP)", + "ERN": "Eritrean nakfa (Nfk)", + "ETB": "Ethiopian birr (Br)", + "EUR": "Euro (€)", + "FJD": "Fijian dollar ($)", + "FKP": "Falkland Islands pound (£)", + "GBP": "Pound sterling (£)", + "GEL": "Georgian lari (₾)", + "GGP": "Guernsey pound (£)", + "GHS": "Ghana cedi (₵)", + "GIP": "Gibraltar pound (£)", + "GMD": "Gambian dalasi (D)", + "GNF": "Guinean franc (Fr)", + "GTQ": "Guatemalan quetzal (Q)", + "GYD": "Guyanese dollar ($)", + "HKD": "Hong Kong dollar ($)", + "HNL": "Honduran lempira (L)", + "HRK": "Croatian kuna (kn)", + "HTG": "Haitian gourde (G)", + "HUF": "Hungarian forint (Ft)", + "IDR": "Indonesian rupiah (Rp)", + "ILS": "Israeli new shekel (₪)", + "IMP": "Manx pound (£)", + "INR": "Indian rupee (₹)", + "IQD": "Iraqi dinar (د.ع)", + "IRR": "Iranian rial (﷼)", + "IRT": "Iranian toman (تومان)", + "ISK": "Icelandic króna (kr.)", + "JEP": "Jersey pound (£)", + "JMD": "Jamaican dollar ($)", + "JOD": "Jordanian dinar (د.ا)", + "JPY": "Japanese yen (¥)", + "KES": "Kenyan shilling (KSh)", + "KGS": "Kyrgyzstani som (сом)", + "KHR": "Cambodian riel (៛)", + "KMF": "Comorian franc (Fr)", + "KPW": "North Korean won (₩)", + "KRW": "South Korean won (₩)", + "KWD": "Kuwaiti dinar (د.ك)", + "KYD": "Cayman Islands dollar ($)", + "KZT": "Kazakhstani tenge (₸)", + "LAK": "Lao kip (₭)", + "LBP": "Lebanese pound (ل.ل)", + "LKR": "Sri Lankan rupee (රු)", + "LRD": "Liberian dollar ($)", + "LSL": "Lesotho loti (L)", + "LYD": "Libyan dinar (ل.د)", + "MAD": "Moroccan dirham (د.م.)", + "MDL": "Moldovan leu (MDL)", + "MGA": "Malagasy ariary (Ar)", + "MKD": "Macedonian denar (ден)", + "MMK": "Burmese kyat (Ks)", + "MNT": "Mongolian tögrög (₮)", + "MOP": "Macanese pataca (P)", + "MRU": "Mauritanian ouguiya (UM)", + "MUR": "Mauritian rupee (₨)", + "MVR": "Maldivian rufiyaa (.ރ)", + "MWK": "Malawian kwacha (MK)", + "MXN": "Mexican peso ($)", + "MYR": "Malaysian ringgit (RM)", + "MZN": "Mozambican metical (MT)", + "NAD": "Namibian dollar (N$)", + "NGN": "Nigerian naira (₦)", + "NIO": "Nicaraguan córdoba (C$)", + "NOK": "Norwegian krone (kr)", + "NPR": "Nepalese rupee (₨)", + "NZD": "New Zealand dollar ($)", + "OMR": "Omani rial (ر.ع.)", + "PAB": "Panamanian balboa (B/.)", + "PEN": "Sol (S/)", + "PGK": "Papua New Guinean kina (K)", + "PHP": "Philippine peso (₱)", + "PKR": "Pakistani rupee (₨)", + "PLN": "Polish złoty (zł)", + "PRB": "Transnistrian ruble (р.)", + "PYG": "Paraguayan guaraní (₲)", + "QAR": "Qatari riyal (ر.ق)", + "RON": "Romanian leu (lei)", + "RSD": "Serbian dinar (рсд)", + "RUB": "Russian ruble (₽)", + "RWF": "Rwandan franc (Fr)", + "SAR": "Saudi riyal (ر.س)", + "SBD": "Solomon Islands dollar ($)", + "SCR": "Seychellois rupee (₨)", + "SDG": "Sudanese pound (ج.س.)", + "SEK": "Swedish krona (kr)", + "SGD": "Singapore dollar ($)", + "SHP": "Saint Helena pound (£)", + "SLL": "Sierra Leonean leone (Le)", + "SOS": "Somali shilling (Sh)", + "SRD": "Surinamese dollar ($)", + "SSP": "South Sudanese pound (£)", + "STN": "São Tomé and Príncipe dobra (Db)", + "SYP": "Syrian pound (ل.س)", + "SZL": "Swazi lilangeni (E)", + "THB": "Thai baht (฿)", + "TJS": "Tajikistani somoni (ЅМ)", + "TMT": "Turkmenistan manat (m)", + "TND": "Tunisian dinar (د.ت)", + "TOP": "Tongan paʻanga (T$)", + "TRY": "Turkish lira (₺)", + "TTD": "Trinidad and Tobago dollar ($)", + "TWD": "New Taiwan dollar (NT$)", + "TZS": "Tanzanian shilling (Sh)", + "UAH": "Ukrainian hryvnia (₴)", + "UGX": "Ugandan shilling (UGX)", + "USD": "United States (US) dollar ($)", + "UYU": "Uruguayan peso ($)", + "UZS": "Uzbekistani som (UZS)", + "VEF": "Venezuelan bolívar (Bs F)", + "VES": "Bolívar soberano (Bs.S)", + "VND": "Vietnamese đồng (₫)", + "VUV": "Vanuatu vatu (Vt)", + "WST": "Samoan tālā (T)", + "XAF": "Central African CFA franc (CFA)", + "XCD": "East Caribbean dollar ($)", + "XOF": "West African CFA franc (CFA)", + "XPF": "CFP franc (Fr)", + "YER": "Yemeni rial (﷼)", + "ZAR": "South African rand (R)", + "ZMW": "Zambian kwacha (ZK)" +}; + +const stateOptions = { + "AF": "Afghanistan", + "AX": "Åland Islands", + "AL:AL-01": "Albania - Berat", + "AL:AL-09": "Albania - Dibër", + "AL:AL-02": "Albania - Durrës", + "AL:AL-03": "Albania - Elbasan", + "AL:AL-04": "Albania - Fier", + "AL:AL-05": "Albania - Gjirokastër", + "AL:AL-06": "Albania - Korçë", + "AL:AL-07": "Albania - Kukës", + "AL:AL-08": "Albania - Lezhë", + "AL:AL-10": "Albania - Shkodër", + "AL:AL-11": "Albania - Tirana", + "AL:AL-12": "Albania - Vlorë", + "DZ:DZ-01": "Algeria - Adrar", + "DZ:DZ-02": "Algeria - Chlef", + "DZ:DZ-03": "Algeria - Laghouat", + "DZ:DZ-04": "Algeria - Oum El Bouaghi", + "DZ:DZ-05": "Algeria - Batna", + "DZ:DZ-06": "Algeria - Béjaïa", + "DZ:DZ-07": "Algeria - Biskra", + "DZ:DZ-08": "Algeria - Béchar", + "DZ:DZ-09": "Algeria - Blida", + "DZ:DZ-10": "Algeria - Bouira", + "DZ:DZ-11": "Algeria - Tamanghasset", + "DZ:DZ-12": "Algeria - Tébessa", + "DZ:DZ-13": "Algeria - Tlemcen", + "DZ:DZ-14": "Algeria - Tiaret", + "DZ:DZ-15": "Algeria - Tizi Ouzou", + "DZ:DZ-16": "Algeria - Algiers", + "DZ:DZ-17": "Algeria - Djelfa", + "DZ:DZ-18": "Algeria - Jijel", + "DZ:DZ-19": "Algeria - Sétif", + "DZ:DZ-20": "Algeria - Saïda", + "DZ:DZ-21": "Algeria - Skikda", + "DZ:DZ-22": "Algeria - Sidi Bel Abbès", + "DZ:DZ-23": "Algeria - Annaba", + "DZ:DZ-24": "Algeria - Guelma", + "DZ:DZ-25": "Algeria - Constantine", + "DZ:DZ-26": "Algeria - Médéa", + "DZ:DZ-27": "Algeria - Mostaganem", + "DZ:DZ-28": "Algeria - M’Sila", + "DZ:DZ-29": "Algeria - Mascara", + "DZ:DZ-30": "Algeria - Ouargla", + "DZ:DZ-31": "Algeria - Oran", + "DZ:DZ-32": "Algeria - El Bayadh", + "DZ:DZ-33": "Algeria - Illizi", + "DZ:DZ-34": "Algeria - Bordj Bou Arréridj", + "DZ:DZ-35": "Algeria - Boumerdès", + "DZ:DZ-36": "Algeria - El Tarf", + "DZ:DZ-37": "Algeria - Tindouf", + "DZ:DZ-38": "Algeria - Tissemsilt", + "DZ:DZ-39": "Algeria - El Oued", + "DZ:DZ-40": "Algeria - Khenchela", + "DZ:DZ-41": "Algeria - Souk Ahras", + "DZ:DZ-42": "Algeria - Tipasa", + "DZ:DZ-43": "Algeria - Mila", + "DZ:DZ-44": "Algeria - Aïn Defla", + "DZ:DZ-45": "Algeria - Naama", + "DZ:DZ-46": "Algeria - Aïn Témouchent", + "DZ:DZ-47": "Algeria - Ghardaïa", + "DZ:DZ-48": "Algeria - Relizane", + "AS": "American Samoa", + "AD": "Andorra", + "AO:BGO": "Angola - Bengo", + "AO:BLU": "Angola - Benguela", + "AO:BIE": "Angola - Bié", + "AO:CAB": "Angola - Cabinda", + "AO:CNN": "Angola - Cunene", + "AO:HUA": "Angola - Huambo", + "AO:HUI": "Angola - Huíla", + "AO:CCU": "Angola - Kuando Kubango", + "AO:CNO": "Angola - Kwanza-Norte", + "AO:CUS": "Angola - Kwanza-Sul", + "AO:LUA": "Angola - Luanda", + "AO:LNO": "Angola - Lunda-Norte", + "AO:LSU": "Angola - Lunda-Sul", + "AO:MAL": "Angola - Malanje", + "AO:MOX": "Angola - Moxico", + "AO:NAM": "Angola - Namibe", + "AO:UIG": "Angola - Uíge", + "AO:ZAI": "Angola - Zaire", + "AI": "Anguilla", + "AQ": "Antarctica", + "AG": "Antigua and Barbuda", + "AR:C": "Argentina - Ciudad Autónoma de Buenos Aires", + "AR:B": "Argentina - Buenos Aires", + "AR:K": "Argentina - Catamarca", + "AR:H": "Argentina - Chaco", + "AR:U": "Argentina - Chubut", + "AR:X": "Argentina - Córdoba", + "AR:W": "Argentina - Corrientes", + "AR:E": "Argentina - Entre Ríos", + "AR:P": "Argentina - Formosa", + "AR:Y": "Argentina - Jujuy", + "AR:L": "Argentina - La Pampa", + "AR:F": "Argentina - La Rioja", + "AR:M": "Argentina - Mendoza", + "AR:N": "Argentina - Misiones", + "AR:Q": "Argentina - Neuquén", + "AR:R": "Argentina - Río Negro", + "AR:A": "Argentina - Salta", + "AR:J": "Argentina - San Juan", + "AR:D": "Argentina - San Luis", + "AR:Z": "Argentina - Santa Cruz", + "AR:S": "Argentina - Santa Fe", + "AR:G": "Argentina - Santiago del Estero", + "AR:V": "Argentina - Tierra del Fuego", + "AR:T": "Argentina - Tucumán", + "AM": "Armenia", + "AW": "Aruba", + "AU:ACT": "Australia - Australian Capital Territory", + "AU:NSW": "Australia - New South Wales", + "AU:NT": "Australia - Northern Territory", + "AU:QLD": "Australia - Queensland", + "AU:SA": "Australia - South Australia", + "AU:TAS": "Australia - Tasmania", + "AU:VIC": "Australia - Victoria", + "AU:WA": "Australia - Western Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BS": "Bahamas", + "BH": "Bahrain", + "BD:BD-05": "Bangladesh - Bagerhat", + "BD:BD-01": "Bangladesh - Bandarban", + "BD:BD-02": "Bangladesh - Barguna", + "BD:BD-06": "Bangladesh - Barishal", + "BD:BD-07": "Bangladesh - Bhola", + "BD:BD-03": "Bangladesh - Bogura", + "BD:BD-04": "Bangladesh - Brahmanbaria", + "BD:BD-09": "Bangladesh - Chandpur", + "BD:BD-10": "Bangladesh - Chattogram", + "BD:BD-12": "Bangladesh - Chuadanga", + "BD:BD-11": "Bangladesh - Cox's Bazar", + "BD:BD-08": "Bangladesh - Cumilla", + "BD:BD-13": "Bangladesh - Dhaka", + "BD:BD-14": "Bangladesh - Dinajpur", + "BD:BD-15": "Bangladesh - Faridpur ", + "BD:BD-16": "Bangladesh - Feni", + "BD:BD-19": "Bangladesh - Gaibandha", + "BD:BD-18": "Bangladesh - Gazipur", + "BD:BD-17": "Bangladesh - Gopalganj", + "BD:BD-20": "Bangladesh - Habiganj", + "BD:BD-21": "Bangladesh - Jamalpur", + "BD:BD-22": "Bangladesh - Jashore", + "BD:BD-25": "Bangladesh - Jhalokati", + "BD:BD-23": "Bangladesh - Jhenaidah", + "BD:BD-24": "Bangladesh - Joypurhat", + "BD:BD-29": "Bangladesh - Khagrachhari", + "BD:BD-27": "Bangladesh - Khulna", + "BD:BD-26": "Bangladesh - Kishoreganj", + "BD:BD-28": "Bangladesh - Kurigram", + "BD:BD-30": "Bangladesh - Kushtia", + "BD:BD-31": "Bangladesh - Lakshmipur", + "BD:BD-32": "Bangladesh - Lalmonirhat", + "BD:BD-36": "Bangladesh - Madaripur", + "BD:BD-37": "Bangladesh - Magura", + "BD:BD-33": "Bangladesh - Manikganj ", + "BD:BD-39": "Bangladesh - Meherpur", + "BD:BD-38": "Bangladesh - Moulvibazar", + "BD:BD-35": "Bangladesh - Munshiganj", + "BD:BD-34": "Bangladesh - Mymensingh", + "BD:BD-48": "Bangladesh - Naogaon", + "BD:BD-43": "Bangladesh - Narail", + "BD:BD-40": "Bangladesh - Narayanganj", + "BD:BD-42": "Bangladesh - Narsingdi", + "BD:BD-44": "Bangladesh - Natore", + "BD:BD-45": "Bangladesh - Nawabganj", + "BD:BD-41": "Bangladesh - Netrakona", + "BD:BD-46": "Bangladesh - Nilphamari", + "BD:BD-47": "Bangladesh - Noakhali", + "BD:BD-49": "Bangladesh - Pabna", + "BD:BD-52": "Bangladesh - Panchagarh", + "BD:BD-51": "Bangladesh - Patuakhali", + "BD:BD-50": "Bangladesh - Pirojpur", + "BD:BD-53": "Bangladesh - Rajbari", + "BD:BD-54": "Bangladesh - Rajshahi", + "BD:BD-56": "Bangladesh - Rangamati", + "BD:BD-55": "Bangladesh - Rangpur", + "BD:BD-58": "Bangladesh - Satkhira", + "BD:BD-62": "Bangladesh - Shariatpur", + "BD:BD-57": "Bangladesh - Sherpur", + "BD:BD-59": "Bangladesh - Sirajganj", + "BD:BD-61": "Bangladesh - Sunamganj", + "BD:BD-60": "Bangladesh - Sylhet", + "BD:BD-63": "Bangladesh - Tangail", + "BD:BD-64": "Bangladesh - Thakurgaon", + "BB": "Barbados", + "BY": "Belarus", + "PW": "Belau", + "BE": "Belgium", + "BZ": "Belize", + "BJ:AL": "Benin - Alibori", + "BJ:AK": "Benin - Atakora", + "BJ:AQ": "Benin - Atlantique", + "BJ:BO": "Benin - Borgou", + "BJ:CO": "Benin - Collines", + "BJ:KO": "Benin - Kouffo", + "BJ:DO": "Benin - Donga", + "BJ:LI": "Benin - Littoral", + "BJ:MO": "Benin - Mono", + "BJ:OU": "Benin - Ouémé", + "BJ:PL": "Benin - Plateau", + "BJ:ZO": "Benin - Zou", + "BM": "Bermuda", + "BT": "Bhutan", + "BO:BO-B": "Bolivia - Beni", + "BO:BO-H": "Bolivia - Chuquisaca", + "BO:BO-C": "Bolivia - Cochabamba", + "BO:BO-L": "Bolivia - La Paz", + "BO:BO-O": "Bolivia - Oruro", + "BO:BO-N": "Bolivia - Pando", + "BO:BO-P": "Bolivia - Potosí", + "BO:BO-S": "Bolivia - Santa Cruz", + "BO:BO-T": "Bolivia - Tarija", + "BQ": "Bonaire, Saint Eustatius and Saba", + "BA": "Bosnia and Herzegovina", + "BW": "Botswana", + "BV": "Bouvet Island", + "BR:AC": "Brazil - Acre", + "BR:AL": "Brazil - Alagoas", + "BR:AP": "Brazil - Amapá", + "BR:AM": "Brazil - Amazonas", + "BR:BA": "Brazil - Bahia", + "BR:CE": "Brazil - Ceará", + "BR:DF": "Brazil - Distrito Federal", + "BR:ES": "Brazil - Espírito Santo", + "BR:GO": "Brazil - Goiás", + "BR:MA": "Brazil - Maranhão", + "BR:MT": "Brazil - Mato Grosso", + "BR:MS": "Brazil - Mato Grosso do Sul", + "BR:MG": "Brazil - Minas Gerais", + "BR:PA": "Brazil - Pará", + "BR:PB": "Brazil - Paraíba", + "BR:PR": "Brazil - Paraná", + "BR:PE": "Brazil - Pernambuco", + "BR:PI": "Brazil - Piauí", + "BR:RJ": "Brazil - Rio de Janeiro", + "BR:RN": "Brazil - Rio Grande do Norte", + "BR:RS": "Brazil - Rio Grande do Sul", + "BR:RO": "Brazil - Rondônia", + "BR:RR": "Brazil - Roraima", + "BR:SC": "Brazil - Santa Catarina", + "BR:SP": "Brazil - São Paulo", + "BR:SE": "Brazil - Sergipe", + "BR:TO": "Brazil - Tocantins", + "IO": "British Indian Ocean Territory", + "BN": "Brunei", + "BG:BG-01": "Bulgaria - Blagoevgrad", + "BG:BG-02": "Bulgaria - Burgas", + "BG:BG-08": "Bulgaria - Dobrich", + "BG:BG-07": "Bulgaria - Gabrovo", + "BG:BG-26": "Bulgaria - Haskovo", + "BG:BG-09": "Bulgaria - Kardzhali", + "BG:BG-10": "Bulgaria - Kyustendil", + "BG:BG-11": "Bulgaria - Lovech", + "BG:BG-12": "Bulgaria - Montana", + "BG:BG-13": "Bulgaria - Pazardzhik", + "BG:BG-14": "Bulgaria - Pernik", + "BG:BG-15": "Bulgaria - Pleven", + "BG:BG-16": "Bulgaria - Plovdiv", + "BG:BG-17": "Bulgaria - Razgrad", + "BG:BG-18": "Bulgaria - Ruse", + "BG:BG-27": "Bulgaria - Shumen", + "BG:BG-19": "Bulgaria - Silistra", + "BG:BG-20": "Bulgaria - Sliven", + "BG:BG-21": "Bulgaria - Smolyan", + "BG:BG-23": "Bulgaria - Sofia District", + "BG:BG-22": "Bulgaria - Sofia", + "BG:BG-24": "Bulgaria - Stara Zagora", + "BG:BG-25": "Bulgaria - Targovishte", + "BG:BG-03": "Bulgaria - Varna", + "BG:BG-04": "Bulgaria - Veliko Tarnovo", + "BG:BG-05": "Bulgaria - Vidin", + "BG:BG-06": "Bulgaria - Vratsa", + "BG:BG-28": "Bulgaria - Yambol", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA:AB": "Canada - Alberta", + "CA:BC": "Canada - British Columbia", + "CA:MB": "Canada - Manitoba", + "CA:NB": "Canada - New Brunswick", + "CA:NL": "Canada - Newfoundland and Labrador", + "CA:NT": "Canada - Northwest Territories", + "CA:NS": "Canada - Nova Scotia", + "CA:NU": "Canada - Nunavut", + "CA:ON": "Canada - Ontario", + "CA:PE": "Canada - Prince Edward Island", + "CA:QC": "Canada - Quebec", + "CA:SK": "Canada - Saskatchewan", + "CA:YT": "Canada - Yukon Territory", + "CV": "Cape Verde", + "KY": "Cayman Islands", + "CF": "Central African Republic", + "TD": "Chad", + "CL:CL-AI": "Chile - Aisén del General Carlos Ibañez del Campo", + "CL:CL-AN": "Chile - Antofagasta", + "CL:CL-AP": "Chile - Arica y Parinacota", + "CL:CL-AR": "Chile - La Araucanía", + "CL:CL-AT": "Chile - Atacama", + "CL:CL-BI": "Chile - Biobío", + "CL:CL-CO": "Chile - Coquimbo", + "CL:CL-LI": "Chile - Libertador General Bernardo O'Higgins", + "CL:CL-LL": "Chile - Los Lagos", + "CL:CL-LR": "Chile - Los Ríos", + "CL:CL-MA": "Chile - Magallanes", + "CL:CL-ML": "Chile - Maule", + "CL:CL-NB": "Chile - Ñuble", + "CL:CL-RM": "Chile - Región Metropolitana de Santiago", + "CL:CL-TA": "Chile - Tarapacá", + "CL:CL-VS": "Chile - Valparaíso", + "CN:CN1": "China - Yunnan / 云南", + "CN:CN2": "China - Beijing / 北京", + "CN:CN3": "China - Tianjin / 天津", + "CN:CN4": "China - Hebei / 河北", + "CN:CN5": "China - Shanxi / 山西", + "CN:CN6": "China - Inner Mongolia / 內蒙古", + "CN:CN7": "China - Liaoning / 辽宁", + "CN:CN8": "China - Jilin / 吉林", + "CN:CN9": "China - Heilongjiang / 黑龙江", + "CN:CN10": "China - Shanghai / 上海", + "CN:CN11": "China - Jiangsu / 江苏", + "CN:CN12": "China - Zhejiang / 浙江", + "CN:CN13": "China - Anhui / 安徽", + "CN:CN14": "China - Fujian / 福建", + "CN:CN15": "China - Jiangxi / 江西", + "CN:CN16": "China - Shandong / 山东", + "CN:CN17": "China - Henan / 河南", + "CN:CN18": "China - Hubei / 湖北", + "CN:CN19": "China - Hunan / 湖南", + "CN:CN20": "China - Guangdong / 广东", + "CN:CN21": "China - Guangxi Zhuang / 广西壮族", + "CN:CN22": "China - Hainan / 海南", + "CN:CN23": "China - Chongqing / 重庆", + "CN:CN24": "China - Sichuan / 四川", + "CN:CN25": "China - Guizhou / 贵州", + "CN:CN26": "China - Shaanxi / 陕西", + "CN:CN27": "China - Gansu / 甘肃", + "CN:CN28": "China - Qinghai / 青海", + "CN:CN29": "China - Ningxia Hui / 宁夏", + "CN:CN30": "China - Macao / 澳门", + "CN:CN31": "China - Tibet / 西藏", + "CN:CN32": "China - Xinjiang / 新疆", + "CX": "Christmas Island", + "CC": "Cocos (Keeling) Islands", + "CO:CO-AMA": "Colombia - Amazonas", + "CO:CO-ANT": "Colombia - Antioquia", + "CO:CO-ARA": "Colombia - Arauca", + "CO:CO-ATL": "Colombia - Atlántico", + "CO:CO-BOL": "Colombia - Bolívar", + "CO:CO-BOY": "Colombia - Boyacá", + "CO:CO-CAL": "Colombia - Caldas", + "CO:CO-CAQ": "Colombia - Caquetá", + "CO:CO-CAS": "Colombia - Casanare", + "CO:CO-CAU": "Colombia - Cauca", + "CO:CO-CES": "Colombia - Cesar", + "CO:CO-CHO": "Colombia - Chocó", + "CO:CO-COR": "Colombia - Córdoba", + "CO:CO-CUN": "Colombia - Cundinamarca", + "CO:CO-DC": "Colombia - Capital District", + "CO:CO-GUA": "Colombia - Guainía", + "CO:CO-GUV": "Colombia - Guaviare", + "CO:CO-HUI": "Colombia - Huila", + "CO:CO-LAG": "Colombia - La Guajira", + "CO:CO-MAG": "Colombia - Magdalena", + "CO:CO-MET": "Colombia - Meta", + "CO:CO-NAR": "Colombia - Nariño", + "CO:CO-NSA": "Colombia - Norte de Santander", + "CO:CO-PUT": "Colombia - Putumayo", + "CO:CO-QUI": "Colombia - Quindío", + "CO:CO-RIS": "Colombia - Risaralda", + "CO:CO-SAN": "Colombia - Santander", + "CO:CO-SAP": "Colombia - San Andrés & Providencia", + "CO:CO-SUC": "Colombia - Sucre", + "CO:CO-TOL": "Colombia - Tolima", + "CO:CO-VAC": "Colombia - Valle del Cauca", + "CO:CO-VAU": "Colombia - Vaupés", + "CO:CO-VID": "Colombia - Vichada", + "KM": "Comoros", + "CG": "Congo (Brazzaville)", + "CD": "Congo (Kinshasa)", + "CK": "Cook Islands", + "CR:CR-A": "Costa Rica - Alajuela", + "CR:CR-C": "Costa Rica - Cartago", + "CR:CR-G": "Costa Rica - Guanacaste", + "CR:CR-H": "Costa Rica - Heredia", + "CR:CR-L": "Costa Rica - Limón", + "CR:CR-P": "Costa Rica - Puntarenas", + "CR:CR-SJ": "Costa Rica - San José", + "HR": "Croatia", + "CU": "Cuba", + "CW": "Curaçao", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DK": "Denmark", + "DJ": "Djibouti", + "DM": "Dominica", + "DO:DO-01": "Dominican Republic - Distrito Nacional", + "DO:DO-02": "Dominican Republic - Azua", + "DO:DO-03": "Dominican Republic - Baoruco", + "DO:DO-04": "Dominican Republic - Barahona", + "DO:DO-33": "Dominican Republic - Cibao Nordeste", + "DO:DO-34": "Dominican Republic - Cibao Noroeste", + "DO:DO-35": "Dominican Republic - Cibao Norte", + "DO:DO-36": "Dominican Republic - Cibao Sur", + "DO:DO-05": "Dominican Republic - Dajabón", + "DO:DO-06": "Dominican Republic - Duarte", + "DO:DO-08": "Dominican Republic - El Seibo", + "DO:DO-37": "Dominican Republic - El Valle", + "DO:DO-07": "Dominican Republic - Elías Piña", + "DO:DO-38": "Dominican Republic - Enriquillo", + "DO:DO-09": "Dominican Republic - Espaillat", + "DO:DO-30": "Dominican Republic - Hato Mayor", + "DO:DO-19": "Dominican Republic - Hermanas Mirabal", + "DO:DO-39": "Dominican Republic - Higüamo", + "DO:DO-10": "Dominican Republic - Independencia", + "DO:DO-11": "Dominican Republic - La Altagracia", + "DO:DO-12": "Dominican Republic - La Romana", + "DO:DO-13": "Dominican Republic - La Vega", + "DO:DO-14": "Dominican Republic - María Trinidad Sánchez", + "DO:DO-28": "Dominican Republic - Monseñor Nouel", + "DO:DO-15": "Dominican Republic - Monte Cristi", + "DO:DO-29": "Dominican Republic - Monte Plata", + "DO:DO-40": "Dominican Republic - Ozama", + "DO:DO-16": "Dominican Republic - Pedernales", + "DO:DO-17": "Dominican Republic - Peravia", + "DO:DO-18": "Dominican Republic - Puerto Plata", + "DO:DO-20": "Dominican Republic - Samaná", + "DO:DO-21": "Dominican Republic - San Cristóbal", + "DO:DO-31": "Dominican Republic - San José de Ocoa", + "DO:DO-22": "Dominican Republic - San Juan", + "DO:DO-23": "Dominican Republic - San Pedro de Macorís", + "DO:DO-24": "Dominican Republic - Sánchez Ramírez", + "DO:DO-25": "Dominican Republic - Santiago", + "DO:DO-26": "Dominican Republic - Santiago Rodríguez", + "DO:DO-32": "Dominican Republic - Santo Domingo", + "DO:DO-41": "Dominican Republic - Valdesia", + "DO:DO-27": "Dominican Republic - Valverde", + "DO:DO-42": "Dominican Republic - Yuma", + "EC:EC-A": "Ecuador - Azuay", + "EC:EC-B": "Ecuador - Bolívar", + "EC:EC-F": "Ecuador - Cañar", + "EC:EC-C": "Ecuador - Carchi", + "EC:EC-H": "Ecuador - Chimborazo", + "EC:EC-X": "Ecuador - Cotopaxi", + "EC:EC-O": "Ecuador - El Oro", + "EC:EC-E": "Ecuador - Esmeraldas", + "EC:EC-W": "Ecuador - Galápagos", + "EC:EC-G": "Ecuador - Guayas", + "EC:EC-I": "Ecuador - Imbabura", + "EC:EC-L": "Ecuador - Loja", + "EC:EC-R": "Ecuador - Los Ríos", + "EC:EC-M": "Ecuador - Manabí", + "EC:EC-S": "Ecuador - Morona-Santiago", + "EC:EC-N": "Ecuador - Napo", + "EC:EC-D": "Ecuador - Orellana", + "EC:EC-Y": "Ecuador - Pastaza", + "EC:EC-P": "Ecuador - Pichincha", + "EC:EC-SE": "Ecuador - Santa Elena", + "EC:EC-SD": "Ecuador - Santo Domingo de los Tsáchilas", + "EC:EC-U": "Ecuador - Sucumbíos", + "EC:EC-T": "Ecuador - Tungurahua", + "EC:EC-Z": "Ecuador - Zamora-Chinchipe", + "EG:EGALX": "Egypt - Alexandria", + "EG:EGASN": "Egypt - Aswan", + "EG:EGAST": "Egypt - Asyut", + "EG:EGBA": "Egypt - Red Sea", + "EG:EGBH": "Egypt - Beheira", + "EG:EGBNS": "Egypt - Beni Suef", + "EG:EGC": "Egypt - Cairo", + "EG:EGDK": "Egypt - Dakahlia", + "EG:EGDT": "Egypt - Damietta", + "EG:EGFYM": "Egypt - Faiyum", + "EG:EGGH": "Egypt - Gharbia", + "EG:EGGZ": "Egypt - Giza", + "EG:EGIS": "Egypt - Ismailia", + "EG:EGJS": "Egypt - South Sinai", + "EG:EGKB": "Egypt - Qalyubia", + "EG:EGKFS": "Egypt - Kafr el-Sheikh", + "EG:EGKN": "Egypt - Qena", + "EG:EGLX": "Egypt - Luxor", + "EG:EGMN": "Egypt - Minya", + "EG:EGMNF": "Egypt - Monufia", + "EG:EGMT": "Egypt - Matrouh", + "EG:EGPTS": "Egypt - Port Said", + "EG:EGSHG": "Egypt - Sohag", + "EG:EGSHR": "Egypt - Al Sharqia", + "EG:EGSIN": "Egypt - North Sinai", + "EG:EGSUZ": "Egypt - Suez", + "EG:EGWAD": "Egypt - New Valley", + "SV:SV-AH": "El Salvador - Ahuachapán", + "SV:SV-CA": "El Salvador - Cabañas", + "SV:SV-CH": "El Salvador - Chalatenango", + "SV:SV-CU": "El Salvador - Cuscatlán", + "SV:SV-LI": "El Salvador - La Libertad", + "SV:SV-MO": "El Salvador - Morazán", + "SV:SV-PA": "El Salvador - La Paz", + "SV:SV-SA": "El Salvador - Santa Ana", + "SV:SV-SM": "El Salvador - San Miguel", + "SV:SV-SO": "El Salvador - Sonsonate", + "SV:SV-SS": "El Salvador - San Salvador", + "SV:SV-SV": "El Salvador - San Vicente", + "SV:SV-UN": "El Salvador - La Unión", + "SV:SV-US": "El Salvador - Usulután", + "GQ": "Equatorial Guinea", + "ER": "Eritrea", + "EE": "Estonia", + "SZ": "Eswatini", + "ET": "Ethiopia", + "FK": "Falkland Islands", + "FO": "Faroe Islands", + "FJ": "Fiji", + "FI": "Finland", + "FR": "France", + "GF": "French Guiana", + "PF": "French Polynesia", + "TF": "French Southern Territories", + "GA": "Gabon", + "GM": "Gambia", + "GE": "Georgia", + "DE:DE-BW": "Germany - Baden-Württemberg", + "DE:DE-BY": "Germany - Bavaria", + "DE:DE-BE": "Germany - Berlin", + "DE:DE-BB": "Germany - Brandenburg", + "DE:DE-HB": "Germany - Bremen", + "DE:DE-HH": "Germany - Hamburg", + "DE:DE-HE": "Germany - Hesse", + "DE:DE-MV": "Germany - Mecklenburg-Vorpommern", + "DE:DE-NI": "Germany - Lower Saxony", + "DE:DE-NW": "Germany - North Rhine-Westphalia", + "DE:DE-RP": "Germany - Rhineland-Palatinate", + "DE:DE-SL": "Germany - Saarland", + "DE:DE-SN": "Germany - Saxony", + "DE:DE-ST": "Germany - Saxony-Anhalt", + "DE:DE-SH": "Germany - Schleswig-Holstein", + "DE:DE-TH": "Germany - Thuringia", + "GH:AF": "Ghana - Ahafo", + "GH:AH": "Ghana - Ashanti", + "GH:BA": "Ghana - Brong-Ahafo", + "GH:BO": "Ghana - Bono", + "GH:BE": "Ghana - Bono East", + "GH:CP": "Ghana - Central", + "GH:EP": "Ghana - Eastern", + "GH:AA": "Ghana - Greater Accra", + "GH:NE": "Ghana - North East", + "GH:NP": "Ghana - Northern", + "GH:OT": "Ghana - Oti", + "GH:SV": "Ghana - Savannah", + "GH:UE": "Ghana - Upper East", + "GH:UW": "Ghana - Upper West", + "GH:TV": "Ghana - Volta", + "GH:WP": "Ghana - Western", + "GH:WN": "Ghana - Western North", + "GI": "Gibraltar", + "GR:I": "Greece - Attica", + "GR:A": "Greece - East Macedonia and Thrace", + "GR:B": "Greece - Central Macedonia", + "GR:C": "Greece - West Macedonia", + "GR:D": "Greece - Epirus", + "GR:E": "Greece - Thessaly", + "GR:F": "Greece - Ionian Islands", + "GR:G": "Greece - West Greece", + "GR:H": "Greece - Central Greece", + "GR:J": "Greece - Peloponnese", + "GR:K": "Greece - North Aegean", + "GR:L": "Greece - South Aegean", + "GR:M": "Greece - Crete", + "GL": "Greenland", + "GD": "Grenada", + "GP": "Guadeloupe", + "GU": "Guam", + "GT:GT-AV": "Guatemala - Alta Verapaz", + "GT:GT-BV": "Guatemala - Baja Verapaz", + "GT:GT-CM": "Guatemala - Chimaltenango", + "GT:GT-CQ": "Guatemala - Chiquimula", + "GT:GT-PR": "Guatemala - El Progreso", + "GT:GT-ES": "Guatemala - Escuintla", + "GT:GT-GU": "Guatemala - Guatemala", + "GT:GT-HU": "Guatemala - Huehuetenango", + "GT:GT-IZ": "Guatemala - Izabal", + "GT:GT-JA": "Guatemala - Jalapa", + "GT:GT-JU": "Guatemala - Jutiapa", + "GT:GT-PE": "Guatemala - Petén", + "GT:GT-QZ": "Guatemala - Quetzaltenango", + "GT:GT-QC": "Guatemala - Quiché", + "GT:GT-RE": "Guatemala - Retalhuleu", + "GT:GT-SA": "Guatemala - Sacatepéquez", + "GT:GT-SM": "Guatemala - San Marcos", + "GT:GT-SR": "Guatemala - Santa Rosa", + "GT:GT-SO": "Guatemala - Sololá", + "GT:GT-SU": "Guatemala - Suchitepéquez", + "GT:GT-TO": "Guatemala - Totonicapán", + "GT:GT-ZA": "Guatemala - Zacapa", + "GG": "Guernsey", + "GN": "Guinea", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HT": "Haiti", + "HM": "Heard Island and McDonald Islands", + "HN:HN-AT": "Honduras - Atlántida", + "HN:HN-IB": "Honduras - Bay Islands", + "HN:HN-CH": "Honduras - Choluteca", + "HN:HN-CL": "Honduras - Colón", + "HN:HN-CM": "Honduras - Comayagua", + "HN:HN-CP": "Honduras - Copán", + "HN:HN-CR": "Honduras - Cortés", + "HN:HN-EP": "Honduras - El Paraíso", + "HN:HN-FM": "Honduras - Francisco Morazán", + "HN:HN-GD": "Honduras - Gracias a Dios", + "HN:HN-IN": "Honduras - Intibucá", + "HN:HN-LE": "Honduras - Lempira", + "HN:HN-LP": "Honduras - La Paz", + "HN:HN-OC": "Honduras - Ocotepeque", + "HN:HN-OL": "Honduras - Olancho", + "HN:HN-SB": "Honduras - Santa Bárbara", + "HN:HN-VA": "Honduras - Valle", + "HN:HN-YO": "Honduras - Yoro", + "HK:HONG KONG": "Hong Kong - Hong Kong Island", + "HK:KOWLOON": "Hong Kong - Kowloon", + "HK:NEW TERRITORIES": "Hong Kong - New Territories", + "HU:BK": "Hungary - Bács-Kiskun", + "HU:BE": "Hungary - Békés", + "HU:BA": "Hungary - Baranya", + "HU:BZ": "Hungary - Borsod-Abaúj-Zemplén", + "HU:BU": "Hungary - Budapest", + "HU:CS": "Hungary - Csongrád-Csanád", + "HU:FE": "Hungary - Fejér", + "HU:GS": "Hungary - Győr-Moson-Sopron", + "HU:HB": "Hungary - Hajdú-Bihar", + "HU:HE": "Hungary - Heves", + "HU:JN": "Hungary - Jász-Nagykun-Szolnok", + "HU:KE": "Hungary - Komárom-Esztergom", + "HU:NO": "Hungary - Nógrád", + "HU:PE": "Hungary - Pest", + "HU:SO": "Hungary - Somogy", + "HU:SZ": "Hungary - Szabolcs-Szatmár-Bereg", + "HU:TO": "Hungary - Tolna", + "HU:VA": "Hungary - Vas", + "HU:VE": "Hungary - Veszprém", + "HU:ZA": "Hungary - Zala", + "IS": "Iceland", + "IN:AP": "India - Andhra Pradesh", + "IN:AR": "India - Arunachal Pradesh", + "IN:AS": "India - Assam", + "IN:BR": "India - Bihar", + "IN:CT": "India - Chhattisgarh", + "IN:GA": "India - Goa", + "IN:GJ": "India - Gujarat", + "IN:HR": "India - Haryana", + "IN:HP": "India - Himachal Pradesh", + "IN:JK": "India - Jammu and Kashmir", + "IN:JH": "India - Jharkhand", + "IN:KA": "India - Karnataka", + "IN:KL": "India - Kerala", + "IN:LA": "India - Ladakh", + "IN:MP": "India - Madhya Pradesh", + "IN:MH": "India - Maharashtra", + "IN:MN": "India - Manipur", + "IN:ML": "India - Meghalaya", + "IN:MZ": "India - Mizoram", + "IN:NL": "India - Nagaland", + "IN:OR": "India - Odisha", + "IN:PB": "India - Punjab", + "IN:RJ": "India - Rajasthan", + "IN:SK": "India - Sikkim", + "IN:TN": "India - Tamil Nadu", + "IN:TS": "India - Telangana", + "IN:TR": "India - Tripura", + "IN:UK": "India - Uttarakhand", + "IN:UP": "India - Uttar Pradesh", + "IN:WB": "India - West Bengal", + "IN:AN": "India - Andaman and Nicobar Islands", + "IN:CH": "India - Chandigarh", + "IN:DN": "India - Dadra and Nagar Haveli", + "IN:DD": "India - Daman and Diu", + "IN:DL": "India - Delhi", + "IN:LD": "India - Lakshadeep", + "IN:PY": "India - Pondicherry (Puducherry)", + "ID:AC": "Indonesia - Daerah Istimewa Aceh", + "ID:SU": "Indonesia - Sumatera Utara", + "ID:SB": "Indonesia - Sumatera Barat", + "ID:RI": "Indonesia - Riau", + "ID:KR": "Indonesia - Kepulauan Riau", + "ID:JA": "Indonesia - Jambi", + "ID:SS": "Indonesia - Sumatera Selatan", + "ID:BB": "Indonesia - Bangka Belitung", + "ID:BE": "Indonesia - Bengkulu", + "ID:LA": "Indonesia - Lampung", + "ID:JK": "Indonesia - DKI Jakarta", + "ID:JB": "Indonesia - Jawa Barat", + "ID:BT": "Indonesia - Banten", + "ID:JT": "Indonesia - Jawa Tengah", + "ID:JI": "Indonesia - Jawa Timur", + "ID:YO": "Indonesia - Daerah Istimewa Yogyakarta", + "ID:BA": "Indonesia - Bali", + "ID:NB": "Indonesia - Nusa Tenggara Barat", + "ID:NT": "Indonesia - Nusa Tenggara Timur", + "ID:KB": "Indonesia - Kalimantan Barat", + "ID:KT": "Indonesia - Kalimantan Tengah", + "ID:KI": "Indonesia - Kalimantan Timur", + "ID:KS": "Indonesia - Kalimantan Selatan", + "ID:KU": "Indonesia - Kalimantan Utara", + "ID:SA": "Indonesia - Sulawesi Utara", + "ID:ST": "Indonesia - Sulawesi Tengah", + "ID:SG": "Indonesia - Sulawesi Tenggara", + "ID:SR": "Indonesia - Sulawesi Barat", + "ID:SN": "Indonesia - Sulawesi Selatan", + "ID:GO": "Indonesia - Gorontalo", + "ID:MA": "Indonesia - Maluku", + "ID:MU": "Indonesia - Maluku Utara", + "ID:PA": "Indonesia - Papua", + "ID:PB": "Indonesia - Papua Barat", + "IR:KHZ": "Iran - Khuzestan (خوزستان)", + "IR:THR": "Iran - Tehran (تهران)", + "IR:ILM": "Iran - Ilaam (ایلام)", + "IR:BHR": "Iran - Bushehr (بوشهر)", + "IR:ADL": "Iran - Ardabil (اردبیل)", + "IR:ESF": "Iran - Isfahan (اصفهان)", + "IR:YZD": "Iran - Yazd (یزد)", + "IR:KRH": "Iran - Kermanshah (کرمانشاه)", + "IR:KRN": "Iran - Kerman (کرمان)", + "IR:HDN": "Iran - Hamadan (همدان)", + "IR:GZN": "Iran - Ghazvin (قزوین)", + "IR:ZJN": "Iran - Zanjan (زنجان)", + "IR:LRS": "Iran - Luristan (لرستان)", + "IR:ABZ": "Iran - Alborz (البرز)", + "IR:EAZ": "Iran - East Azarbaijan (آذربایجان شرقی)", + "IR:WAZ": "Iran - West Azarbaijan (آذربایجان غربی)", + "IR:CHB": "Iran - Chaharmahal and Bakhtiari (چهارمحال و بختیاری)", + "IR:SKH": "Iran - South Khorasan (خراسان جنوبی)", + "IR:RKH": "Iran - Razavi Khorasan (خراسان رضوی)", + "IR:NKH": "Iran - North Khorasan (خراسان شمالی)", + "IR:SMN": "Iran - Semnan (سمنان)", + "IR:FRS": "Iran - Fars (فارس)", + "IR:QHM": "Iran - Qom (قم)", + "IR:KRD": "Iran - Kurdistan / کردستان)", + "IR:KBD": "Iran - Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)", + "IR:GLS": "Iran - Golestan (گلستان)", + "IR:GIL": "Iran - Gilan (گیلان)", + "IR:MZN": "Iran - Mazandaran (مازندران)", + "IR:MKZ": "Iran - Markazi (مرکزی)", + "IR:HRZ": "Iran - Hormozgan (هرمزگان)", + "IR:SBN": "Iran - Sistan and Baluchestan (سیستان و بلوچستان)", + "IQ": "Iraq", + "IE:CW": "Ireland - Carlow", + "IE:CN": "Ireland - Cavan", + "IE:CE": "Ireland - Clare", + "IE:CO": "Ireland - Cork", + "IE:DL": "Ireland - Donegal", + "IE:D": "Ireland - Dublin", + "IE:G": "Ireland - Galway", + "IE:KY": "Ireland - Kerry", + "IE:KE": "Ireland - Kildare", + "IE:KK": "Ireland - Kilkenny", + "IE:LS": "Ireland - Laois", + "IE:LM": "Ireland - Leitrim", + "IE:LK": "Ireland - Limerick", + "IE:LD": "Ireland - Longford", + "IE:LH": "Ireland - Louth", + "IE:MO": "Ireland - Mayo", + "IE:MH": "Ireland - Meath", + "IE:MN": "Ireland - Monaghan", + "IE:OY": "Ireland - Offaly", + "IE:RN": "Ireland - Roscommon", + "IE:SO": "Ireland - Sligo", + "IE:TA": "Ireland - Tipperary", + "IE:WD": "Ireland - Waterford", + "IE:WH": "Ireland - Westmeath", + "IE:WX": "Ireland - Wexford", + "IE:WW": "Ireland - Wicklow", + "IM": "Isle of Man", + "IL": "Israel", + "IT:AG": "Italy - Agrigento", + "IT:AL": "Italy - Alessandria", + "IT:AN": "Italy - Ancona", + "IT:AO": "Italy - Aosta", + "IT:AR": "Italy - Arezzo", + "IT:AP": "Italy - Ascoli Piceno", + "IT:AT": "Italy - Asti", + "IT:AV": "Italy - Avellino", + "IT:BA": "Italy - Bari", + "IT:BT": "Italy - Barletta-Andria-Trani", + "IT:BL": "Italy - Belluno", + "IT:BN": "Italy - Benevento", + "IT:BG": "Italy - Bergamo", + "IT:BI": "Italy - Biella", + "IT:BO": "Italy - Bologna", + "IT:BZ": "Italy - Bolzano", + "IT:BS": "Italy - Brescia", + "IT:BR": "Italy - Brindisi", + "IT:CA": "Italy - Cagliari", + "IT:CL": "Italy - Caltanissetta", + "IT:CB": "Italy - Campobasso", + "IT:CE": "Italy - Caserta", + "IT:CT": "Italy - Catania", + "IT:CZ": "Italy - Catanzaro", + "IT:CH": "Italy - Chieti", + "IT:CO": "Italy - Como", + "IT:CS": "Italy - Cosenza", + "IT:CR": "Italy - Cremona", + "IT:KR": "Italy - Crotone", + "IT:CN": "Italy - Cuneo", + "IT:EN": "Italy - Enna", + "IT:FM": "Italy - Fermo", + "IT:FE": "Italy - Ferrara", + "IT:FI": "Italy - Firenze", + "IT:FG": "Italy - Foggia", + "IT:FC": "Italy - Forlì-Cesena", + "IT:FR": "Italy - Frosinone", + "IT:GE": "Italy - Genova", + "IT:GO": "Italy - Gorizia", + "IT:GR": "Italy - Grosseto", + "IT:IM": "Italy - Imperia", + "IT:IS": "Italy - Isernia", + "IT:SP": "Italy - La Spezia", + "IT:AQ": "Italy - L'Aquila", + "IT:LT": "Italy - Latina", + "IT:LE": "Italy - Lecce", + "IT:LC": "Italy - Lecco", + "IT:LI": "Italy - Livorno", + "IT:LO": "Italy - Lodi", + "IT:LU": "Italy - Lucca", + "IT:MC": "Italy - Macerata", + "IT:MN": "Italy - Mantova", + "IT:MS": "Italy - Massa-Carrara", + "IT:MT": "Italy - Matera", + "IT:ME": "Italy - Messina", + "IT:MI": "Italy - Milano", + "IT:MO": "Italy - Modena", + "IT:MB": "Italy - Monza e della Brianza", + "IT:NA": "Italy - Napoli", + "IT:NO": "Italy - Novara", + "IT:NU": "Italy - Nuoro", + "IT:OR": "Italy - Oristano", + "IT:PD": "Italy - Padova", + "IT:PA": "Italy - Palermo", + "IT:PR": "Italy - Parma", + "IT:PV": "Italy - Pavia", + "IT:PG": "Italy - Perugia", + "IT:PU": "Italy - Pesaro e Urbino", + "IT:PE": "Italy - Pescara", + "IT:PC": "Italy - Piacenza", + "IT:PI": "Italy - Pisa", + "IT:PT": "Italy - Pistoia", + "IT:PN": "Italy - Pordenone", + "IT:PZ": "Italy - Potenza", + "IT:PO": "Italy - Prato", + "IT:RG": "Italy - Ragusa", + "IT:RA": "Italy - Ravenna", + "IT:RC": "Italy - Reggio Calabria", + "IT:RE": "Italy - Reggio Emilia", + "IT:RI": "Italy - Rieti", + "IT:RN": "Italy - Rimini", + "IT:RM": "Italy - Roma", + "IT:RO": "Italy - Rovigo", + "IT:SA": "Italy - Salerno", + "IT:SS": "Italy - Sassari", + "IT:SV": "Italy - Savona", + "IT:SI": "Italy - Siena", + "IT:SR": "Italy - Siracusa", + "IT:SO": "Italy - Sondrio", + "IT:SU": "Italy - Sud Sardegna", + "IT:TA": "Italy - Taranto", + "IT:TE": "Italy - Teramo", + "IT:TR": "Italy - Terni", + "IT:TO": "Italy - Torino", + "IT:TP": "Italy - Trapani", + "IT:TN": "Italy - Trento", + "IT:TV": "Italy - Treviso", + "IT:TS": "Italy - Trieste", + "IT:UD": "Italy - Udine", + "IT:VA": "Italy - Varese", + "IT:VE": "Italy - Venezia", + "IT:VB": "Italy - Verbano-Cusio-Ossola", + "IT:VC": "Italy - Vercelli", + "IT:VR": "Italy - Verona", + "IT:VV": "Italy - Vibo Valentia", + "IT:VI": "Italy - Vicenza", + "IT:VT": "Italy - Viterbo", + "CI": "Ivory Coast", + "JM:JM-01": "Jamaica - Kingston", + "JM:JM-02": "Jamaica - Saint Andrew", + "JM:JM-03": "Jamaica - Saint Thomas", + "JM:JM-04": "Jamaica - Portland", + "JM:JM-05": "Jamaica - Saint Mary", + "JM:JM-06": "Jamaica - Saint Ann", + "JM:JM-07": "Jamaica - Trelawny", + "JM:JM-08": "Jamaica - Saint James", + "JM:JM-09": "Jamaica - Hanover", + "JM:JM-10": "Jamaica - Westmoreland", + "JM:JM-11": "Jamaica - Saint Elizabeth", + "JM:JM-12": "Jamaica - Manchester", + "JM:JM-13": "Jamaica - Clarendon", + "JM:JM-14": "Jamaica - Saint Catherine", + "JP:JP01": "Japan - Hokkaido", + "JP:JP02": "Japan - Aomori", + "JP:JP03": "Japan - Iwate", + "JP:JP04": "Japan - Miyagi", + "JP:JP05": "Japan - Akita", + "JP:JP06": "Japan - Yamagata", + "JP:JP07": "Japan - Fukushima", + "JP:JP08": "Japan - Ibaraki", + "JP:JP09": "Japan - Tochigi", + "JP:JP10": "Japan - Gunma", + "JP:JP11": "Japan - Saitama", + "JP:JP12": "Japan - Chiba", + "JP:JP13": "Japan - Tokyo", + "JP:JP14": "Japan - Kanagawa", + "JP:JP15": "Japan - Niigata", + "JP:JP16": "Japan - Toyama", + "JP:JP17": "Japan - Ishikawa", + "JP:JP18": "Japan - Fukui", + "JP:JP19": "Japan - Yamanashi", + "JP:JP20": "Japan - Nagano", + "JP:JP21": "Japan - Gifu", + "JP:JP22": "Japan - Shizuoka", + "JP:JP23": "Japan - Aichi", + "JP:JP24": "Japan - Mie", + "JP:JP25": "Japan - Shiga", + "JP:JP26": "Japan - Kyoto", + "JP:JP27": "Japan - Osaka", + "JP:JP28": "Japan - Hyogo", + "JP:JP29": "Japan - Nara", + "JP:JP30": "Japan - Wakayama", + "JP:JP31": "Japan - Tottori", + "JP:JP32": "Japan - Shimane", + "JP:JP33": "Japan - Okayama", + "JP:JP34": "Japan - Hiroshima", + "JP:JP35": "Japan - Yamaguchi", + "JP:JP36": "Japan - Tokushima", + "JP:JP37": "Japan - Kagawa", + "JP:JP38": "Japan - Ehime", + "JP:JP39": "Japan - Kochi", + "JP:JP40": "Japan - Fukuoka", + "JP:JP41": "Japan - Saga", + "JP:JP42": "Japan - Nagasaki", + "JP:JP43": "Japan - Kumamoto", + "JP:JP44": "Japan - Oita", + "JP:JP45": "Japan - Miyazaki", + "JP:JP46": "Japan - Kagoshima", + "JP:JP47": "Japan - Okinawa", + "JE": "Jersey", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE:KE01": "Kenya - Baringo", + "KE:KE02": "Kenya - Bomet", + "KE:KE03": "Kenya - Bungoma", + "KE:KE04": "Kenya - Busia", + "KE:KE05": "Kenya - Elgeyo-Marakwet", + "KE:KE06": "Kenya - Embu", + "KE:KE07": "Kenya - Garissa", + "KE:KE08": "Kenya - Homa Bay", + "KE:KE09": "Kenya - Isiolo", + "KE:KE10": "Kenya - Kajiado", + "KE:KE11": "Kenya - Kakamega", + "KE:KE12": "Kenya - Kericho", + "KE:KE13": "Kenya - Kiambu", + "KE:KE14": "Kenya - Kilifi", + "KE:KE15": "Kenya - Kirinyaga", + "KE:KE16": "Kenya - Kisii", + "KE:KE17": "Kenya - Kisumu", + "KE:KE18": "Kenya - Kitui", + "KE:KE19": "Kenya - Kwale", + "KE:KE20": "Kenya - Laikipia", + "KE:KE21": "Kenya - Lamu", + "KE:KE22": "Kenya - Machakos", + "KE:KE23": "Kenya - Makueni", + "KE:KE24": "Kenya - Mandera", + "KE:KE25": "Kenya - Marsabit", + "KE:KE26": "Kenya - Meru", + "KE:KE27": "Kenya - Migori", + "KE:KE28": "Kenya - Mombasa", + "KE:KE29": "Kenya - Murang’a", + "KE:KE30": "Kenya - Nairobi County", + "KE:KE31": "Kenya - Nakuru", + "KE:KE32": "Kenya - Nandi", + "KE:KE33": "Kenya - Narok", + "KE:KE34": "Kenya - Nyamira", + "KE:KE35": "Kenya - Nyandarua", + "KE:KE36": "Kenya - Nyeri", + "KE:KE37": "Kenya - Samburu", + "KE:KE38": "Kenya - Siaya", + "KE:KE39": "Kenya - Taita-Taveta", + "KE:KE40": "Kenya - Tana River", + "KE:KE41": "Kenya - Tharaka-Nithi", + "KE:KE42": "Kenya - Trans Nzoia", + "KE:KE43": "Kenya - Turkana", + "KE:KE44": "Kenya - Uasin Gishu", + "KE:KE45": "Kenya - Vihiga", + "KE:KE46": "Kenya - Wajir", + "KE:KE47": "Kenya - West Pokot", + "KI": "Kiribati", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LA:AT": "Laos - Attapeu", + "LA:BK": "Laos - Bokeo", + "LA:BL": "Laos - Bolikhamsai", + "LA:CH": "Laos - Champasak", + "LA:HO": "Laos - Houaphanh", + "LA:KH": "Laos - Khammouane", + "LA:LM": "Laos - Luang Namtha", + "LA:LP": "Laos - Luang Prabang", + "LA:OU": "Laos - Oudomxay", + "LA:PH": "Laos - Phongsaly", + "LA:SL": "Laos - Salavan", + "LA:SV": "Laos - Savannakhet", + "LA:VI": "Laos - Vientiane Province", + "LA:VT": "Laos - Vientiane", + "LA:XA": "Laos - Sainyabuli", + "LA:XE": "Laos - Sekong", + "LA:XI": "Laos - Xiangkhouang", + "LA:XS": "Laos - Xaisomboun", + "LV": "Latvia", + "LB": "Lebanon", + "LS": "Lesotho", + "LR:BM": "Liberia - Bomi", + "LR:BN": "Liberia - Bong", + "LR:GA": "Liberia - Gbarpolu", + "LR:GB": "Liberia - Grand Bassa", + "LR:GC": "Liberia - Grand Cape Mount", + "LR:GG": "Liberia - Grand Gedeh", + "LR:GK": "Liberia - Grand Kru", + "LR:LO": "Liberia - Lofa", + "LR:MA": "Liberia - Margibi", + "LR:MY": "Liberia - Maryland", + "LR:MO": "Liberia - Montserrado", + "LR:NM": "Liberia - Nimba", + "LR:RV": "Liberia - Rivercess", + "LR:RG": "Liberia - River Gee", + "LR:SN": "Liberia - Sinoe", + "LY": "Libya", + "LI": "Liechtenstein", + "LT": "Lithuania", + "LU": "Luxembourg", + "MO": "Macao", + "MG": "Madagascar", + "MW": "Malawi", + "MY:JHR": "Malaysia - Johor", + "MY:KDH": "Malaysia - Kedah", + "MY:KTN": "Malaysia - Kelantan", + "MY:LBN": "Malaysia - Labuan", + "MY:MLK": "Malaysia - Malacca (Melaka)", + "MY:NSN": "Malaysia - Negeri Sembilan", + "MY:PHG": "Malaysia - Pahang", + "MY:PNG": "Malaysia - Penang (Pulau Pinang)", + "MY:PRK": "Malaysia - Perak", + "MY:PLS": "Malaysia - Perlis", + "MY:SBH": "Malaysia - Sabah", + "MY:SWK": "Malaysia - Sarawak", + "MY:SGR": "Malaysia - Selangor", + "MY:TRG": "Malaysia - Terengganu", + "MY:PJY": "Malaysia - Putrajaya", + "MY:KUL": "Malaysia - Kuala Lumpur", + "MV": "Maldives", + "ML": "Mali", + "MT": "Malta", + "MH": "Marshall Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MU": "Mauritius", + "YT": "Mayotte", + "MX:DF": "Mexico - Ciudad de México", + "MX:JA": "Mexico - Jalisco", + "MX:NL": "Mexico - Nuevo León", + "MX:AG": "Mexico - Aguascalientes", + "MX:BC": "Mexico - Baja California", + "MX:BS": "Mexico - Baja California Sur", + "MX:CM": "Mexico - Campeche", + "MX:CS": "Mexico - Chiapas", + "MX:CH": "Mexico - Chihuahua", + "MX:CO": "Mexico - Coahuila", + "MX:CL": "Mexico - Colima", + "MX:DG": "Mexico - Durango", + "MX:GT": "Mexico - Guanajuato", + "MX:GR": "Mexico - Guerrero", + "MX:HG": "Mexico - Hidalgo", + "MX:MX": "Mexico - Estado de México", + "MX:MI": "Mexico - Michoacán", + "MX:MO": "Mexico - Morelos", + "MX:NA": "Mexico - Nayarit", + "MX:OA": "Mexico - Oaxaca", + "MX:PU": "Mexico - Puebla", + "MX:QT": "Mexico - Querétaro", + "MX:QR": "Mexico - Quintana Roo", + "MX:SL": "Mexico - San Luis Potosí", + "MX:SI": "Mexico - Sinaloa", + "MX:SO": "Mexico - Sonora", + "MX:TB": "Mexico - Tabasco", + "MX:TM": "Mexico - Tamaulipas", + "MX:TL": "Mexico - Tlaxcala", + "MX:VE": "Mexico - Veracruz", + "MX:YU": "Mexico - Yucatán", + "MX:ZA": "Mexico - Zacatecas", + "FM": "Micronesia", + "MD:C": "Moldova - Chișinău", + "MD:BL": "Moldova - Bălți", + "MD:AN": "Moldova - Anenii Noi", + "MD:BS": "Moldova - Basarabeasca", + "MD:BR": "Moldova - Briceni", + "MD:CH": "Moldova - Cahul", + "MD:CT": "Moldova - Cantemir", + "MD:CL": "Moldova - Călărași", + "MD:CS": "Moldova - Căușeni", + "MD:CM": "Moldova - Cimișlia", + "MD:CR": "Moldova - Criuleni", + "MD:DN": "Moldova - Dondușeni", + "MD:DR": "Moldova - Drochia", + "MD:DB": "Moldova - Dubăsari", + "MD:ED": "Moldova - Edineț", + "MD:FL": "Moldova - Fălești", + "MD:FR": "Moldova - Florești", + "MD:GE": "Moldova - UTA Găgăuzia", + "MD:GL": "Moldova - Glodeni", + "MD:HN": "Moldova - Hîncești", + "MD:IL": "Moldova - Ialoveni", + "MD:LV": "Moldova - Leova", + "MD:NS": "Moldova - Nisporeni", + "MD:OC": "Moldova - Ocnița", + "MD:OR": "Moldova - Orhei", + "MD:RZ": "Moldova - Rezina", + "MD:RS": "Moldova - Rîșcani", + "MD:SG": "Moldova - Sîngerei", + "MD:SR": "Moldova - Soroca", + "MD:ST": "Moldova - Strășeni", + "MD:SD": "Moldova - Șoldănești", + "MD:SV": "Moldova - Ștefan Vodă", + "MD:TR": "Moldova - Taraclia", + "MD:TL": "Moldova - Telenești", + "MD:UN": "Moldova - Ungheni", + "MC": "Monaco", + "MN": "Mongolia", + "ME": "Montenegro", + "MS": "Montserrat", + "MA": "Morocco", + "MZ:MZP": "Mozambique - Cabo Delgado", + "MZ:MZG": "Mozambique - Gaza", + "MZ:MZI": "Mozambique - Inhambane", + "MZ:MZB": "Mozambique - Manica", + "MZ:MZL": "Mozambique - Maputo Province", + "MZ:MZMPM": "Mozambique - Maputo", + "MZ:MZN": "Mozambique - Nampula", + "MZ:MZA": "Mozambique - Niassa", + "MZ:MZS": "Mozambique - Sofala", + "MZ:MZT": "Mozambique - Tete", + "MZ:MZQ": "Mozambique - Zambézia", + "MM": "Myanmar", + "NA:ER": "Namibia - Erongo", + "NA:HA": "Namibia - Hardap", + "NA:KA": "Namibia - Karas", + "NA:KE": "Namibia - Kavango East", + "NA:KW": "Namibia - Kavango West", + "NA:KH": "Namibia - Khomas", + "NA:KU": "Namibia - Kunene", + "NA:OW": "Namibia - Ohangwena", + "NA:OH": "Namibia - Omaheke", + "NA:OS": "Namibia - Omusati", + "NA:ON": "Namibia - Oshana", + "NA:OT": "Namibia - Oshikoto", + "NA:OD": "Namibia - Otjozondjupa", + "NA:CA": "Namibia - Zambezi", + "NR": "Nauru", + "NP:BAG": "Nepal - Bagmati", + "NP:BHE": "Nepal - Bheri", + "NP:DHA": "Nepal - Dhaulagiri", + "NP:GAN": "Nepal - Gandaki", + "NP:JAN": "Nepal - Janakpur", + "NP:KAR": "Nepal - Karnali", + "NP:KOS": "Nepal - Koshi", + "NP:LUM": "Nepal - Lumbini", + "NP:MAH": "Nepal - Mahakali", + "NP:MEC": "Nepal - Mechi", + "NP:NAR": "Nepal - Narayani", + "NP:RAP": "Nepal - Rapti", + "NP:SAG": "Nepal - Sagarmatha", + "NP:SET": "Nepal - Seti", + "NL": "Netherlands", + "NC": "New Caledonia", + "NZ:NTL": "New Zealand - Northland", + "NZ:AUK": "New Zealand - Auckland", + "NZ:WKO": "New Zealand - Waikato", + "NZ:BOP": "New Zealand - Bay of Plenty", + "NZ:TKI": "New Zealand - Taranaki", + "NZ:GIS": "New Zealand - Gisborne", + "NZ:HKB": "New Zealand - Hawke’s Bay", + "NZ:MWT": "New Zealand - Manawatu-Wanganui", + "NZ:WGN": "New Zealand - Wellington", + "NZ:NSN": "New Zealand - Nelson", + "NZ:MBH": "New Zealand - Marlborough", + "NZ:TAS": "New Zealand - Tasman", + "NZ:WTC": "New Zealand - West Coast", + "NZ:CAN": "New Zealand - Canterbury", + "NZ:OTA": "New Zealand - Otago", + "NZ:STL": "New Zealand - Southland", + "NI:NI-AN": "Nicaragua - Atlántico Norte", + "NI:NI-AS": "Nicaragua - Atlántico Sur", + "NI:NI-BO": "Nicaragua - Boaco", + "NI:NI-CA": "Nicaragua - Carazo", + "NI:NI-CI": "Nicaragua - Chinandega", + "NI:NI-CO": "Nicaragua - Chontales", + "NI:NI-ES": "Nicaragua - Estelí", + "NI:NI-GR": "Nicaragua - Granada", + "NI:NI-JI": "Nicaragua - Jinotega", + "NI:NI-LE": "Nicaragua - León", + "NI:NI-MD": "Nicaragua - Madriz", + "NI:NI-MN": "Nicaragua - Managua", + "NI:NI-MS": "Nicaragua - Masaya", + "NI:NI-MT": "Nicaragua - Matagalpa", + "NI:NI-NS": "Nicaragua - Nueva Segovia", + "NI:NI-RI": "Nicaragua - Rivas", + "NI:NI-SJ": "Nicaragua - Río San Juan", + "NE": "Niger", + "NG:AB": "Nigeria - Abia", + "NG:FC": "Nigeria - Abuja", + "NG:AD": "Nigeria - Adamawa", + "NG:AK": "Nigeria - Akwa Ibom", + "NG:AN": "Nigeria - Anambra", + "NG:BA": "Nigeria - Bauchi", + "NG:BY": "Nigeria - Bayelsa", + "NG:BE": "Nigeria - Benue", + "NG:BO": "Nigeria - Borno", + "NG:CR": "Nigeria - Cross River", + "NG:DE": "Nigeria - Delta", + "NG:EB": "Nigeria - Ebonyi", + "NG:ED": "Nigeria - Edo", + "NG:EK": "Nigeria - Ekiti", + "NG:EN": "Nigeria - Enugu", + "NG:GO": "Nigeria - Gombe", + "NG:IM": "Nigeria - Imo", + "NG:JI": "Nigeria - Jigawa", + "NG:KD": "Nigeria - Kaduna", + "NG:KN": "Nigeria - Kano", + "NG:KT": "Nigeria - Katsina", + "NG:KE": "Nigeria - Kebbi", + "NG:KO": "Nigeria - Kogi", + "NG:KW": "Nigeria - Kwara", + "NG:LA": "Nigeria - Lagos", + "NG:NA": "Nigeria - Nasarawa", + "NG:NI": "Nigeria - Niger", + "NG:OG": "Nigeria - Ogun", + "NG:ON": "Nigeria - Ondo", + "NG:OS": "Nigeria - Osun", + "NG:OY": "Nigeria - Oyo", + "NG:PL": "Nigeria - Plateau", + "NG:RI": "Nigeria - Rivers", + "NG:SO": "Nigeria - Sokoto", + "NG:TA": "Nigeria - Taraba", + "NG:YO": "Nigeria - Yobe", + "NG:ZA": "Nigeria - Zamfara", + "NU": "Niue", + "NF": "Norfolk Island", + "KP": "North Korea", + "MK": "North Macedonia", + "MP": "Northern Mariana Islands", + "NO": "Norway", + "OM": "Oman", + "PK:JK": "Pakistan - Azad Kashmir", + "PK:BA": "Pakistan - Balochistan", + "PK:TA": "Pakistan - FATA", + "PK:GB": "Pakistan - Gilgit Baltistan", + "PK:IS": "Pakistan - Islamabad Capital Territory", + "PK:KP": "Pakistan - Khyber Pakhtunkhwa", + "PK:PB": "Pakistan - Punjab", + "PK:SD": "Pakistan - Sindh", + "PS": "Palestinian Territory", + "PA:PA-1": "Panama - Bocas del Toro", + "PA:PA-2": "Panama - Coclé", + "PA:PA-3": "Panama - Colón", + "PA:PA-4": "Panama - Chiriquí", + "PA:PA-5": "Panama - Darién", + "PA:PA-6": "Panama - Herrera", + "PA:PA-7": "Panama - Los Santos", + "PA:PA-8": "Panama - Panamá", + "PA:PA-9": "Panama - Veraguas", + "PA:PA-10": "Panama - West Panamá", + "PA:PA-EM": "Panama - Emberá", + "PA:PA-KY": "Panama - Guna Yala", + "PA:PA-NB": "Panama - Ngöbe-Buglé", + "PG": "Papua New Guinea", + "PY:PY-ASU": "Paraguay - Asunción", + "PY:PY-1": "Paraguay - Concepción", + "PY:PY-2": "Paraguay - San Pedro", + "PY:PY-3": "Paraguay - Cordillera", + "PY:PY-4": "Paraguay - Guairá", + "PY:PY-5": "Paraguay - Caaguazú", + "PY:PY-6": "Paraguay - Caazapá", + "PY:PY-7": "Paraguay - Itapúa", + "PY:PY-8": "Paraguay - Misiones", + "PY:PY-9": "Paraguay - Paraguarí", + "PY:PY-10": "Paraguay - Alto Paraná", + "PY:PY-11": "Paraguay - Central", + "PY:PY-12": "Paraguay - Ñeembucú", + "PY:PY-13": "Paraguay - Amambay", + "PY:PY-14": "Paraguay - Canindeyú", + "PY:PY-15": "Paraguay - Presidente Hayes", + "PY:PY-16": "Paraguay - Alto Paraguay", + "PY:PY-17": "Paraguay - Boquerón", + "PE:CAL": "Peru - El Callao", + "PE:LMA": "Peru - Municipalidad Metropolitana de Lima", + "PE:AMA": "Peru - Amazonas", + "PE:ANC": "Peru - Ancash", + "PE:APU": "Peru - Apurímac", + "PE:ARE": "Peru - Arequipa", + "PE:AYA": "Peru - Ayacucho", + "PE:CAJ": "Peru - Cajamarca", + "PE:CUS": "Peru - Cusco", + "PE:HUV": "Peru - Huancavelica", + "PE:HUC": "Peru - Huánuco", + "PE:ICA": "Peru - Ica", + "PE:JUN": "Peru - Junín", + "PE:LAL": "Peru - La Libertad", + "PE:LAM": "Peru - Lambayeque", + "PE:LIM": "Peru - Lima", + "PE:LOR": "Peru - Loreto", + "PE:MDD": "Peru - Madre de Dios", + "PE:MOQ": "Peru - Moquegua", + "PE:PAS": "Peru - Pasco", + "PE:PIU": "Peru - Piura", + "PE:PUN": "Peru - Puno", + "PE:SAM": "Peru - San Martín", + "PE:TAC": "Peru - Tacna", + "PE:TUM": "Peru - Tumbes", + "PE:UCA": "Peru - Ucayali", + "PH:ABR": "Philippines - Abra", + "PH:AGN": "Philippines - Agusan del Norte", + "PH:AGS": "Philippines - Agusan del Sur", + "PH:AKL": "Philippines - Aklan", + "PH:ALB": "Philippines - Albay", + "PH:ANT": "Philippines - Antique", + "PH:APA": "Philippines - Apayao", + "PH:AUR": "Philippines - Aurora", + "PH:BAS": "Philippines - Basilan", + "PH:BAN": "Philippines - Bataan", + "PH:BTN": "Philippines - Batanes", + "PH:BTG": "Philippines - Batangas", + "PH:BEN": "Philippines - Benguet", + "PH:BIL": "Philippines - Biliran", + "PH:BOH": "Philippines - Bohol", + "PH:BUK": "Philippines - Bukidnon", + "PH:BUL": "Philippines - Bulacan", + "PH:CAG": "Philippines - Cagayan", + "PH:CAN": "Philippines - Camarines Norte", + "PH:CAS": "Philippines - Camarines Sur", + "PH:CAM": "Philippines - Camiguin", + "PH:CAP": "Philippines - Capiz", + "PH:CAT": "Philippines - Catanduanes", + "PH:CAV": "Philippines - Cavite", + "PH:CEB": "Philippines - Cebu", + "PH:COM": "Philippines - Compostela Valley", + "PH:NCO": "Philippines - Cotabato", + "PH:DAV": "Philippines - Davao del Norte", + "PH:DAS": "Philippines - Davao del Sur", + "PH:DAC": "Philippines - Davao Occidental", + "PH:DAO": "Philippines - Davao Oriental", + "PH:DIN": "Philippines - Dinagat Islands", + "PH:EAS": "Philippines - Eastern Samar", + "PH:GUI": "Philippines - Guimaras", + "PH:IFU": "Philippines - Ifugao", + "PH:ILN": "Philippines - Ilocos Norte", + "PH:ILS": "Philippines - Ilocos Sur", + "PH:ILI": "Philippines - Iloilo", + "PH:ISA": "Philippines - Isabela", + "PH:KAL": "Philippines - Kalinga", + "PH:LUN": "Philippines - La Union", + "PH:LAG": "Philippines - Laguna", + "PH:LAN": "Philippines - Lanao del Norte", + "PH:LAS": "Philippines - Lanao del Sur", + "PH:LEY": "Philippines - Leyte", + "PH:MAG": "Philippines - Maguindanao", + "PH:MAD": "Philippines - Marinduque", + "PH:MAS": "Philippines - Masbate", + "PH:MSC": "Philippines - Misamis Occidental", + "PH:MSR": "Philippines - Misamis Oriental", + "PH:MOU": "Philippines - Mountain Province", + "PH:NEC": "Philippines - Negros Occidental", + "PH:NER": "Philippines - Negros Oriental", + "PH:NSA": "Philippines - Northern Samar", + "PH:NUE": "Philippines - Nueva Ecija", + "PH:NUV": "Philippines - Nueva Vizcaya", + "PH:MDC": "Philippines - Occidental Mindoro", + "PH:MDR": "Philippines - Oriental Mindoro", + "PH:PLW": "Philippines - Palawan", + "PH:PAM": "Philippines - Pampanga", + "PH:PAN": "Philippines - Pangasinan", + "PH:QUE": "Philippines - Quezon", + "PH:QUI": "Philippines - Quirino", + "PH:RIZ": "Philippines - Rizal", + "PH:ROM": "Philippines - Romblon", + "PH:WSA": "Philippines - Samar", + "PH:SAR": "Philippines - Sarangani", + "PH:SIQ": "Philippines - Siquijor", + "PH:SOR": "Philippines - Sorsogon", + "PH:SCO": "Philippines - South Cotabato", + "PH:SLE": "Philippines - Southern Leyte", + "PH:SUK": "Philippines - Sultan Kudarat", + "PH:SLU": "Philippines - Sulu", + "PH:SUN": "Philippines - Surigao del Norte", + "PH:SUR": "Philippines - Surigao del Sur", + "PH:TAR": "Philippines - Tarlac", + "PH:TAW": "Philippines - Tawi-Tawi", + "PH:ZMB": "Philippines - Zambales", + "PH:ZAN": "Philippines - Zamboanga del Norte", + "PH:ZAS": "Philippines - Zamboanga del Sur", + "PH:ZSI": "Philippines - Zamboanga Sibugay", + "PH:00": "Philippines - Metro Manila", + "PN": "Pitcairn", + "PL": "Poland", + "PT": "Portugal", + "PR": "Puerto Rico", + "QA": "Qatar", + "RE": "Reunion", + "RO:AB": "Romania - Alba", + "RO:AR": "Romania - Arad", + "RO:AG": "Romania - Argeș", + "RO:BC": "Romania - Bacău", + "RO:BH": "Romania - Bihor", + "RO:BN": "Romania - Bistrița-Năsăud", + "RO:BT": "Romania - Botoșani", + "RO:BR": "Romania - Brăila", + "RO:BV": "Romania - Brașov", + "RO:B": "Romania - București", + "RO:BZ": "Romania - Buzău", + "RO:CL": "Romania - Călărași", + "RO:CS": "Romania - Caraș-Severin", + "RO:CJ": "Romania - Cluj", + "RO:CT": "Romania - Constanța", + "RO:CV": "Romania - Covasna", + "RO:DB": "Romania - Dâmbovița", + "RO:DJ": "Romania - Dolj", + "RO:GL": "Romania - Galați", + "RO:GR": "Romania - Giurgiu", + "RO:GJ": "Romania - Gorj", + "RO:HR": "Romania - Harghita", + "RO:HD": "Romania - Hunedoara", + "RO:IL": "Romania - Ialomița", + "RO:IS": "Romania - Iași", + "RO:IF": "Romania - Ilfov", + "RO:MM": "Romania - Maramureș", + "RO:MH": "Romania - Mehedinți", + "RO:MS": "Romania - Mureș", + "RO:NT": "Romania - Neamț", + "RO:OT": "Romania - Olt", + "RO:PH": "Romania - Prahova", + "RO:SJ": "Romania - Sălaj", + "RO:SM": "Romania - Satu Mare", + "RO:SB": "Romania - Sibiu", + "RO:SV": "Romania - Suceava", + "RO:TR": "Romania - Teleorman", + "RO:TM": "Romania - Timiș", + "RO:TL": "Romania - Tulcea", + "RO:VL": "Romania - Vâlcea", + "RO:VS": "Romania - Vaslui", + "RO:VN": "Romania - Vrancea", + "RU": "Russia", + "RW": "Rwanda", + "ST": "São Tomé and Príncipe", + "BL": "Saint Barthélemy", + "SH": "Saint Helena", + "KN": "Saint Kitts and Nevis", + "LC": "Saint Lucia", + "SX": "Saint Martin (Dutch part)", + "MF": "Saint Martin (French part)", + "PM": "Saint Pierre and Miquelon", + "VC": "Saint Vincent and the Grenadines", + "WS": "Samoa", + "SM": "San Marino", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS:RS00": "Serbia - Belgrade", + "RS:RS14": "Serbia - Bor", + "RS:RS11": "Serbia - Braničevo", + "RS:RS02": "Serbia - Central Banat", + "RS:RS10": "Serbia - Danube", + "RS:RS23": "Serbia - Jablanica", + "RS:RS09": "Serbia - Kolubara", + "RS:RS08": "Serbia - Mačva", + "RS:RS17": "Serbia - Morava", + "RS:RS20": "Serbia - Nišava", + "RS:RS01": "Serbia - North Bačka", + "RS:RS03": "Serbia - North Banat", + "RS:RS24": "Serbia - Pčinja", + "RS:RS22": "Serbia - Pirot", + "RS:RS13": "Serbia - Pomoravlje", + "RS:RS19": "Serbia - Rasina", + "RS:RS18": "Serbia - Raška", + "RS:RS06": "Serbia - South Bačka", + "RS:RS04": "Serbia - South Banat", + "RS:RS07": "Serbia - Srem", + "RS:RS12": "Serbia - Šumadija", + "RS:RS21": "Serbia - Toplica", + "RS:RS05": "Serbia - West Bačka", + "RS:RS15": "Serbia - Zaječar", + "RS:RS16": "Serbia - Zlatibor", + "RS:RS25": "Serbia - Kosovo", + "RS:RS26": "Serbia - Peć", + "RS:RS27": "Serbia - Prizren", + "RS:RS28": "Serbia - Kosovska Mitrovica", + "RS:RS29": "Serbia - Kosovo-Pomoravlje", + "RS:RSKM": "Serbia - Kosovo-Metohija", + "RS:RSVO": "Serbia - Vojvodina", + "SC": "Seychelles", + "SL": "Sierra Leone", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "SB": "Solomon Islands", + "SO": "Somalia", + "ZA:EC": "South Africa - Eastern Cape", + "ZA:FS": "South Africa - Free State", + "ZA:GP": "South Africa - Gauteng", + "ZA:KZN": "South Africa - KwaZulu-Natal", + "ZA:LP": "South Africa - Limpopo", + "ZA:MP": "South Africa - Mpumalanga", + "ZA:NC": "South Africa - Northern Cape", + "ZA:NW": "South Africa - North West", + "ZA:WC": "South Africa - Western Cape", + "GS": "South Georgia/Sandwich Islands", + "KR": "South Korea", + "SS": "South Sudan", + "ES:C": "Spain - A Coruña", + "ES:VI": "Spain - Araba/Álava", + "ES:AB": "Spain - Albacete", + "ES:A": "Spain - Alicante", + "ES:AL": "Spain - Almería", + "ES:O": "Spain - Asturias", + "ES:AV": "Spain - Ávila", + "ES:BA": "Spain - Badajoz", + "ES:PM": "Spain - Baleares", + "ES:B": "Spain - Barcelona", + "ES:BU": "Spain - Burgos", + "ES:CC": "Spain - Cáceres", + "ES:CA": "Spain - Cádiz", + "ES:S": "Spain - Cantabria", + "ES:CS": "Spain - Castellón", + "ES:CE": "Spain - Ceuta", + "ES:CR": "Spain - Ciudad Real", + "ES:CO": "Spain - Córdoba", + "ES:CU": "Spain - Cuenca", + "ES:GI": "Spain - Girona", + "ES:GR": "Spain - Granada", + "ES:GU": "Spain - Guadalajara", + "ES:SS": "Spain - Gipuzkoa", + "ES:H": "Spain - Huelva", + "ES:HU": "Spain - Huesca", + "ES:J": "Spain - Jaén", + "ES:LO": "Spain - La Rioja", + "ES:GC": "Spain - Las Palmas", + "ES:LE": "Spain - León", + "ES:L": "Spain - Lleida", + "ES:LU": "Spain - Lugo", + "ES:M": "Spain - Madrid", + "ES:MA": "Spain - Málaga", + "ES:ML": "Spain - Melilla", + "ES:MU": "Spain - Murcia", + "ES:NA": "Spain - Navarra", + "ES:OR": "Spain - Ourense", + "ES:P": "Spain - Palencia", + "ES:PO": "Spain - Pontevedra", + "ES:SA": "Spain - Salamanca", + "ES:TF": "Spain - Santa Cruz de Tenerife", + "ES:SG": "Spain - Segovia", + "ES:SE": "Spain - Sevilla", + "ES:SO": "Spain - Soria", + "ES:T": "Spain - Tarragona", + "ES:TE": "Spain - Teruel", + "ES:TO": "Spain - Toledo", + "ES:V": "Spain - Valencia", + "ES:VA": "Spain - Valladolid", + "ES:BI": "Spain - Biscay", + "ES:ZA": "Spain - Zamora", + "ES:Z": "Spain - Zaragoza", + "LK": "Sri Lanka", + "SD": "Sudan", + "SR": "Suriname", + "SJ": "Svalbard and Jan Mayen", + "SE": "Sweden", + "CH:AG": "Switzerland - Aargau", + "CH:AR": "Switzerland - Appenzell Ausserrhoden", + "CH:AI": "Switzerland - Appenzell Innerrhoden", + "CH:BL": "Switzerland - Basel-Landschaft", + "CH:BS": "Switzerland - Basel-Stadt", + "CH:BE": "Switzerland - Bern", + "CH:FR": "Switzerland - Fribourg", + "CH:GE": "Switzerland - Geneva", + "CH:GL": "Switzerland - Glarus", + "CH:GR": "Switzerland - Graubünden", + "CH:JU": "Switzerland - Jura", + "CH:LU": "Switzerland - Luzern", + "CH:NE": "Switzerland - Neuchâtel", + "CH:NW": "Switzerland - Nidwalden", + "CH:OW": "Switzerland - Obwalden", + "CH:SH": "Switzerland - Schaffhausen", + "CH:SZ": "Switzerland - Schwyz", + "CH:SO": "Switzerland - Solothurn", + "CH:SG": "Switzerland - St. Gallen", + "CH:TG": "Switzerland - Thurgau", + "CH:TI": "Switzerland - Ticino", + "CH:UR": "Switzerland - Uri", + "CH:VS": "Switzerland - Valais", + "CH:VD": "Switzerland - Vaud", + "CH:ZG": "Switzerland - Zug", + "CH:ZH": "Switzerland - Zürich", + "SY": "Syria", + "TW": "Taiwan", + "TJ": "Tajikistan", + "TZ:TZ01": "Tanzania - Arusha", + "TZ:TZ02": "Tanzania - Dar es Salaam", + "TZ:TZ03": "Tanzania - Dodoma", + "TZ:TZ04": "Tanzania - Iringa", + "TZ:TZ05": "Tanzania - Kagera", + "TZ:TZ06": "Tanzania - Pemba North", + "TZ:TZ07": "Tanzania - Zanzibar North", + "TZ:TZ08": "Tanzania - Kigoma", + "TZ:TZ09": "Tanzania - Kilimanjaro", + "TZ:TZ10": "Tanzania - Pemba South", + "TZ:TZ11": "Tanzania - Zanzibar South", + "TZ:TZ12": "Tanzania - Lindi", + "TZ:TZ13": "Tanzania - Mara", + "TZ:TZ14": "Tanzania - Mbeya", + "TZ:TZ15": "Tanzania - Zanzibar West", + "TZ:TZ16": "Tanzania - Morogoro", + "TZ:TZ17": "Tanzania - Mtwara", + "TZ:TZ18": "Tanzania - Mwanza", + "TZ:TZ19": "Tanzania - Coast", + "TZ:TZ20": "Tanzania - Rukwa", + "TZ:TZ21": "Tanzania - Ruvuma", + "TZ:TZ22": "Tanzania - Shinyanga", + "TZ:TZ23": "Tanzania - Singida", + "TZ:TZ24": "Tanzania - Tabora", + "TZ:TZ25": "Tanzania - Tanga", + "TZ:TZ26": "Tanzania - Manyara", + "TZ:TZ27": "Tanzania - Geita", + "TZ:TZ28": "Tanzania - Katavi", + "TZ:TZ29": "Tanzania - Njombe", + "TZ:TZ30": "Tanzania - Simiyu", + "TH:TH-37": "Thailand - Amnat Charoen", + "TH:TH-15": "Thailand - Ang Thong", + "TH:TH-14": "Thailand - Ayutthaya", + "TH:TH-10": "Thailand - Bangkok", + "TH:TH-38": "Thailand - Bueng Kan", + "TH:TH-31": "Thailand - Buri Ram", + "TH:TH-24": "Thailand - Chachoengsao", + "TH:TH-18": "Thailand - Chai Nat", + "TH:TH-36": "Thailand - Chaiyaphum", + "TH:TH-22": "Thailand - Chanthaburi", + "TH:TH-50": "Thailand - Chiang Mai", + "TH:TH-57": "Thailand - Chiang Rai", + "TH:TH-20": "Thailand - Chonburi", + "TH:TH-86": "Thailand - Chumphon", + "TH:TH-46": "Thailand - Kalasin", + "TH:TH-62": "Thailand - Kamphaeng Phet", + "TH:TH-71": "Thailand - Kanchanaburi", + "TH:TH-40": "Thailand - Khon Kaen", + "TH:TH-81": "Thailand - Krabi", + "TH:TH-52": "Thailand - Lampang", + "TH:TH-51": "Thailand - Lamphun", + "TH:TH-42": "Thailand - Loei", + "TH:TH-16": "Thailand - Lopburi", + "TH:TH-58": "Thailand - Mae Hong Son", + "TH:TH-44": "Thailand - Maha Sarakham", + "TH:TH-49": "Thailand - Mukdahan", + "TH:TH-26": "Thailand - Nakhon Nayok", + "TH:TH-73": "Thailand - Nakhon Pathom", + "TH:TH-48": "Thailand - Nakhon Phanom", + "TH:TH-30": "Thailand - Nakhon Ratchasima", + "TH:TH-60": "Thailand - Nakhon Sawan", + "TH:TH-80": "Thailand - Nakhon Si Thammarat", + "TH:TH-55": "Thailand - Nan", + "TH:TH-96": "Thailand - Narathiwat", + "TH:TH-39": "Thailand - Nong Bua Lam Phu", + "TH:TH-43": "Thailand - Nong Khai", + "TH:TH-12": "Thailand - Nonthaburi", + "TH:TH-13": "Thailand - Pathum Thani", + "TH:TH-94": "Thailand - Pattani", + "TH:TH-82": "Thailand - Phang Nga", + "TH:TH-93": "Thailand - Phatthalung", + "TH:TH-56": "Thailand - Phayao", + "TH:TH-67": "Thailand - Phetchabun", + "TH:TH-76": "Thailand - Phetchaburi", + "TH:TH-66": "Thailand - Phichit", + "TH:TH-65": "Thailand - Phitsanulok", + "TH:TH-54": "Thailand - Phrae", + "TH:TH-83": "Thailand - Phuket", + "TH:TH-25": "Thailand - Prachin Buri", + "TH:TH-77": "Thailand - Prachuap Khiri Khan", + "TH:TH-85": "Thailand - Ranong", + "TH:TH-70": "Thailand - Ratchaburi", + "TH:TH-21": "Thailand - Rayong", + "TH:TH-45": "Thailand - Roi Et", + "TH:TH-27": "Thailand - Sa Kaeo", + "TH:TH-47": "Thailand - Sakon Nakhon", + "TH:TH-11": "Thailand - Samut Prakan", + "TH:TH-74": "Thailand - Samut Sakhon", + "TH:TH-75": "Thailand - Samut Songkhram", + "TH:TH-19": "Thailand - Saraburi", + "TH:TH-91": "Thailand - Satun", + "TH:TH-17": "Thailand - Sing Buri", + "TH:TH-33": "Thailand - Sisaket", + "TH:TH-90": "Thailand - Songkhla", + "TH:TH-64": "Thailand - Sukhothai", + "TH:TH-72": "Thailand - Suphan Buri", + "TH:TH-84": "Thailand - Surat Thani", + "TH:TH-32": "Thailand - Surin", + "TH:TH-63": "Thailand - Tak", + "TH:TH-92": "Thailand - Trang", + "TH:TH-23": "Thailand - Trat", + "TH:TH-34": "Thailand - Ubon Ratchathani", + "TH:TH-41": "Thailand - Udon Thani", + "TH:TH-61": "Thailand - Uthai Thani", + "TH:TH-53": "Thailand - Uttaradit", + "TH:TH-95": "Thailand - Yala", + "TH:TH-35": "Thailand - Yasothon", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad and Tobago", + "TN": "Tunisia", + "TR:TR01": "Turkey - Adana", + "TR:TR02": "Turkey - Adıyaman", + "TR:TR03": "Turkey - Afyon", + "TR:TR04": "Turkey - Ağrı", + "TR:TR05": "Turkey - Amasya", + "TR:TR06": "Turkey - Ankara", + "TR:TR07": "Turkey - Antalya", + "TR:TR08": "Turkey - Artvin", + "TR:TR09": "Turkey - Aydın", + "TR:TR10": "Turkey - Balıkesir", + "TR:TR11": "Turkey - Bilecik", + "TR:TR12": "Turkey - Bingöl", + "TR:TR13": "Turkey - Bitlis", + "TR:TR14": "Turkey - Bolu", + "TR:TR15": "Turkey - Burdur", + "TR:TR16": "Turkey - Bursa", + "TR:TR17": "Turkey - Çanakkale", + "TR:TR18": "Turkey - Çankırı", + "TR:TR19": "Turkey - Çorum", + "TR:TR20": "Turkey - Denizli", + "TR:TR21": "Turkey - Diyarbakır", + "TR:TR22": "Turkey - Edirne", + "TR:TR23": "Turkey - Elazığ", + "TR:TR24": "Turkey - Erzincan", + "TR:TR25": "Turkey - Erzurum", + "TR:TR26": "Turkey - Eskişehir", + "TR:TR27": "Turkey - Gaziantep", + "TR:TR28": "Turkey - Giresun", + "TR:TR29": "Turkey - Gümüşhane", + "TR:TR30": "Turkey - Hakkari", + "TR:TR31": "Turkey - Hatay", + "TR:TR32": "Turkey - Isparta", + "TR:TR33": "Turkey - İçel", + "TR:TR34": "Turkey - İstanbul", + "TR:TR35": "Turkey - İzmir", + "TR:TR36": "Turkey - Kars", + "TR:TR37": "Turkey - Kastamonu", + "TR:TR38": "Turkey - Kayseri", + "TR:TR39": "Turkey - Kırklareli", + "TR:TR40": "Turkey - Kırşehir", + "TR:TR41": "Turkey - Kocaeli", + "TR:TR42": "Turkey - Konya", + "TR:TR43": "Turkey - Kütahya", + "TR:TR44": "Turkey - Malatya", + "TR:TR45": "Turkey - Manisa", + "TR:TR46": "Turkey - Kahramanmaraş", + "TR:TR47": "Turkey - Mardin", + "TR:TR48": "Turkey - Muğla", + "TR:TR49": "Turkey - Muş", + "TR:TR50": "Turkey - Nevşehir", + "TR:TR51": "Turkey - Niğde", + "TR:TR52": "Turkey - Ordu", + "TR:TR53": "Turkey - Rize", + "TR:TR54": "Turkey - Sakarya", + "TR:TR55": "Turkey - Samsun", + "TR:TR56": "Turkey - Siirt", + "TR:TR57": "Turkey - Sinop", + "TR:TR58": "Turkey - Sivas", + "TR:TR59": "Turkey - Tekirdağ", + "TR:TR60": "Turkey - Tokat", + "TR:TR61": "Turkey - Trabzon", + "TR:TR62": "Turkey - Tunceli", + "TR:TR63": "Turkey - Şanlıurfa", + "TR:TR64": "Turkey - Uşak", + "TR:TR65": "Turkey - Van", + "TR:TR66": "Turkey - Yozgat", + "TR:TR67": "Turkey - Zonguldak", + "TR:TR68": "Turkey - Aksaray", + "TR:TR69": "Turkey - Bayburt", + "TR:TR70": "Turkey - Karaman", + "TR:TR71": "Turkey - Kırıkkale", + "TR:TR72": "Turkey - Batman", + "TR:TR73": "Turkey - Şırnak", + "TR:TR74": "Turkey - Bartın", + "TR:TR75": "Turkey - Ardahan", + "TR:TR76": "Turkey - Iğdır", + "TR:TR77": "Turkey - Yalova", + "TR:TR78": "Turkey - Karabük", + "TR:TR79": "Turkey - Kilis", + "TR:TR80": "Turkey - Osmaniye", + "TR:TR81": "Turkey - Düzce", + "TM": "Turkmenistan", + "TC": "Turks and Caicos Islands", + "TV": "Tuvalu", + "UG:UG314": "Uganda - Abim", + "UG:UG301": "Uganda - Adjumani", + "UG:UG322": "Uganda - Agago", + "UG:UG323": "Uganda - Alebtong", + "UG:UG315": "Uganda - Amolatar", + "UG:UG324": "Uganda - Amudat", + "UG:UG216": "Uganda - Amuria", + "UG:UG316": "Uganda - Amuru", + "UG:UG302": "Uganda - Apac", + "UG:UG303": "Uganda - Arua", + "UG:UG217": "Uganda - Budaka", + "UG:UG218": "Uganda - Bududa", + "UG:UG201": "Uganda - Bugiri", + "UG:UG235": "Uganda - Bugweri", + "UG:UG420": "Uganda - Buhweju", + "UG:UG117": "Uganda - Buikwe", + "UG:UG219": "Uganda - Bukedea", + "UG:UG118": "Uganda - Bukomansimbi", + "UG:UG220": "Uganda - Bukwa", + "UG:UG225": "Uganda - Bulambuli", + "UG:UG416": "Uganda - Buliisa", + "UG:UG401": "Uganda - Bundibugyo", + "UG:UG430": "Uganda - Bunyangabu", + "UG:UG402": "Uganda - Bushenyi", + "UG:UG202": "Uganda - Busia", + "UG:UG221": "Uganda - Butaleja", + "UG:UG119": "Uganda - Butambala", + "UG:UG233": "Uganda - Butebo", + "UG:UG120": "Uganda - Buvuma", + "UG:UG226": "Uganda - Buyende", + "UG:UG317": "Uganda - Dokolo", + "UG:UG121": "Uganda - Gomba", + "UG:UG304": "Uganda - Gulu", + "UG:UG403": "Uganda - Hoima", + "UG:UG417": "Uganda - Ibanda", + "UG:UG203": "Uganda - Iganga", + "UG:UG418": "Uganda - Isingiro", + "UG:UG204": "Uganda - Jinja", + "UG:UG318": "Uganda - Kaabong", + "UG:UG404": "Uganda - Kabale", + "UG:UG405": "Uganda - Kabarole", + "UG:UG213": "Uganda - Kaberamaido", + "UG:UG427": "Uganda - Kagadi", + "UG:UG428": "Uganda - Kakumiro", + "UG:UG101": "Uganda - Kalangala", + "UG:UG222": "Uganda - Kaliro", + "UG:UG122": "Uganda - Kalungu", + "UG:UG102": "Uganda - Kampala", + "UG:UG205": "Uganda - Kamuli", + "UG:UG413": "Uganda - Kamwenge", + "UG:UG414": "Uganda - Kanungu", + "UG:UG206": "Uganda - Kapchorwa", + "UG:UG236": "Uganda - Kapelebyong", + "UG:UG126": "Uganda - Kasanda", + "UG:UG406": "Uganda - Kasese", + "UG:UG207": "Uganda - Katakwi", + "UG:UG112": "Uganda - Kayunga", + "UG:UG407": "Uganda - Kibaale", + "UG:UG103": "Uganda - Kiboga", + "UG:UG227": "Uganda - Kibuku", + "UG:UG432": "Uganda - Kikuube", + "UG:UG419": "Uganda - Kiruhura", + "UG:UG421": "Uganda - Kiryandongo", + "UG:UG408": "Uganda - Kisoro", + "UG:UG305": "Uganda - Kitgum", + "UG:UG319": "Uganda - Koboko", + "UG:UG325": "Uganda - Kole", + "UG:UG306": "Uganda - Kotido", + "UG:UG208": "Uganda - Kumi", + "UG:UG333": "Uganda - Kwania", + "UG:UG228": "Uganda - Kween", + "UG:UG123": "Uganda - Kyankwanzi", + "UG:UG422": "Uganda - Kyegegwa", + "UG:UG415": "Uganda - Kyenjojo", + "UG:UG125": "Uganda - Kyotera", + "UG:UG326": "Uganda - Lamwo", + "UG:UG307": "Uganda - Lira", + "UG:UG229": "Uganda - Luuka", + "UG:UG104": "Uganda - Luwero", + "UG:UG124": "Uganda - Lwengo", + "UG:UG114": "Uganda - Lyantonde", + "UG:UG223": "Uganda - Manafwa", + "UG:UG320": "Uganda - Maracha", + "UG:UG105": "Uganda - Masaka", + "UG:UG409": "Uganda - Masindi", + "UG:UG214": "Uganda - Mayuge", + "UG:UG209": "Uganda - Mbale", + "UG:UG410": "Uganda - Mbarara", + "UG:UG423": "Uganda - Mitooma", + "UG:UG115": "Uganda - Mityana", + "UG:UG308": "Uganda - Moroto", + "UG:UG309": "Uganda - Moyo", + "UG:UG106": "Uganda - Mpigi", + "UG:UG107": "Uganda - Mubende", + "UG:UG108": "Uganda - Mukono", + "UG:UG334": "Uganda - Nabilatuk", + "UG:UG311": "Uganda - Nakapiripirit", + "UG:UG116": "Uganda - Nakaseke", + "UG:UG109": "Uganda - Nakasongola", + "UG:UG230": "Uganda - Namayingo", + "UG:UG234": "Uganda - Namisindwa", + "UG:UG224": "Uganda - Namutumba", + "UG:UG327": "Uganda - Napak", + "UG:UG310": "Uganda - Nebbi", + "UG:UG231": "Uganda - Ngora", + "UG:UG424": "Uganda - Ntoroko", + "UG:UG411": "Uganda - Ntungamo", + "UG:UG328": "Uganda - Nwoya", + "UG:UG331": "Uganda - Omoro", + "UG:UG329": "Uganda - Otuke", + "UG:UG321": "Uganda - Oyam", + "UG:UG312": "Uganda - Pader", + "UG:UG332": "Uganda - Pakwach", + "UG:UG210": "Uganda - Pallisa", + "UG:UG110": "Uganda - Rakai", + "UG:UG429": "Uganda - Rubanda", + "UG:UG425": "Uganda - Rubirizi", + "UG:UG431": "Uganda - Rukiga", + "UG:UG412": "Uganda - Rukungiri", + "UG:UG111": "Uganda - Sembabule", + "UG:UG232": "Uganda - Serere", + "UG:UG426": "Uganda - Sheema", + "UG:UG215": "Uganda - Sironko", + "UG:UG211": "Uganda - Soroti", + "UG:UG212": "Uganda - Tororo", + "UG:UG113": "Uganda - Wakiso", + "UG:UG313": "Uganda - Yumbe", + "UG:UG330": "Uganda - Zombo", + "UA:VN": "Ukraine - Vinnytsia Oblast", + "UA:VL": "Ukraine - Volyn Oblast", + "UA:DP": "Ukraine - Dnipropetrovsk Oblast", + "UA:DT": "Ukraine - Donetsk Oblast", + "UA:ZT": "Ukraine - Zhytomyr Oblast", + "UA:ZK": "Ukraine - Zakarpattia Oblast", + "UA:ZP": "Ukraine - Zaporizhzhia Oblast", + "UA:IF": "Ukraine - Ivano-Frankivsk Oblast", + "UA:KV": "Ukraine - Kyiv Oblast", + "UA:KH": "Ukraine - Kirovohrad Oblast", + "UA:LH": "Ukraine - Luhansk Oblast", + "UA:LV": "Ukraine - Lviv Oblast", + "UA:MY": "Ukraine - Mykolaiv Oblast", + "UA:OD": "Ukraine - Odessa Oblast", + "UA:PL": "Ukraine - Poltava Oblast", + "UA:RV": "Ukraine - Rivne Oblast", + "UA:SM": "Ukraine - Sumy Oblast", + "UA:TP": "Ukraine - Ternopil Oblast", + "UA:KK": "Ukraine - Kharkiv Oblast", + "UA:KS": "Ukraine - Kherson Oblast", + "UA:KM": "Ukraine - Khmelnytskyi Oblast", + "UA:CK": "Ukraine - Cherkasy Oblast", + "UA:CH": "Ukraine - Chernihiv Oblast", + "UA:CV": "Ukraine - Chernivtsi Oblast", + "AE": "United Arab Emirates", + "GB": "United Kingdom (UK)", + "US:AL": "United States (US) - Alabama", + "US:AK": "United States (US) - Alaska", + "US:AZ": "United States (US) - Arizona", + "US:AR": "United States (US) - Arkansas", + "US:CA": "United States (US) - California", + "US:CO": "United States (US) - Colorado", + "US:CT": "United States (US) - Connecticut", + "US:DE": "United States (US) - Delaware", + "US:DC": "United States (US) - District Of Columbia", + "US:FL": "United States (US) - Florida", + "US:GA": "United States (US) - Georgia", + "US:HI": "United States (US) - Hawaii", + "US:ID": "United States (US) - Idaho", + "US:IL": "United States (US) - Illinois", + "US:IN": "United States (US) - Indiana", + "US:IA": "United States (US) - Iowa", + "US:KS": "United States (US) - Kansas", + "US:KY": "United States (US) - Kentucky", + "US:LA": "United States (US) - Louisiana", + "US:ME": "United States (US) - Maine", + "US:MD": "United States (US) - Maryland", + "US:MA": "United States (US) - Massachusetts", + "US:MI": "United States (US) - Michigan", + "US:MN": "United States (US) - Minnesota", + "US:MS": "United States (US) - Mississippi", + "US:MO": "United States (US) - Missouri", + "US:MT": "United States (US) - Montana", + "US:NE": "United States (US) - Nebraska", + "US:NV": "United States (US) - Nevada", + "US:NH": "United States (US) - New Hampshire", + "US:NJ": "United States (US) - New Jersey", + "US:NM": "United States (US) - New Mexico", + "US:NY": "United States (US) - New York", + "US:NC": "United States (US) - North Carolina", + "US:ND": "United States (US) - North Dakota", + "US:OH": "United States (US) - Ohio", + "US:OK": "United States (US) - Oklahoma", + "US:OR": "United States (US) - Oregon", + "US:PA": "United States (US) - Pennsylvania", + "US:RI": "United States (US) - Rhode Island", + "US:SC": "United States (US) - South Carolina", + "US:SD": "United States (US) - South Dakota", + "US:TN": "United States (US) - Tennessee", + "US:TX": "United States (US) - Texas", + "US:UT": "United States (US) - Utah", + "US:VT": "United States (US) - Vermont", + "US:VA": "United States (US) - Virginia", + "US:WA": "United States (US) - Washington", + "US:WV": "United States (US) - West Virginia", + "US:WI": "United States (US) - Wisconsin", + "US:WY": "United States (US) - Wyoming", + "US:AA": "United States (US) - Armed Forces (AA)", + "US:AE": "United States (US) - Armed Forces (AE)", + "US:AP": "United States (US) - Armed Forces (AP)", + "UM:81": "United States (US) Minor Outlying Islands - Baker Island", + "UM:84": "United States (US) Minor Outlying Islands - Howland Island", + "UM:86": "United States (US) Minor Outlying Islands - Jarvis Island", + "UM:67": "United States (US) Minor Outlying Islands - Johnston Atoll", + "UM:89": "United States (US) Minor Outlying Islands - Kingman Reef", + "UM:71": "United States (US) Minor Outlying Islands - Midway Atoll", + "UM:76": "United States (US) Minor Outlying Islands - Navassa Island", + "UM:95": "United States (US) Minor Outlying Islands - Palmyra Atoll", + "UM:79": "United States (US) Minor Outlying Islands - Wake Island", + "UY:UY-AR": "Uruguay - Artigas", + "UY:UY-CA": "Uruguay - Canelones", + "UY:UY-CL": "Uruguay - Cerro Largo", + "UY:UY-CO": "Uruguay - Colonia", + "UY:UY-DU": "Uruguay - Durazno", + "UY:UY-FS": "Uruguay - Flores", + "UY:UY-FD": "Uruguay - Florida", + "UY:UY-LA": "Uruguay - Lavalleja", + "UY:UY-MA": "Uruguay - Maldonado", + "UY:UY-MO": "Uruguay - Montevideo", + "UY:UY-PA": "Uruguay - Paysandú", + "UY:UY-RN": "Uruguay - Río Negro", + "UY:UY-RV": "Uruguay - Rivera", + "UY:UY-RO": "Uruguay - Rocha", + "UY:UY-SA": "Uruguay - Salto", + "UY:UY-SJ": "Uruguay - San José", + "UY:UY-SO": "Uruguay - Soriano", + "UY:UY-TA": "Uruguay - Tacuarembó", + "UY:UY-TT": "Uruguay - Treinta y Tres", + "UZ": "Uzbekistan", + "VU": "Vanuatu", + "VA": "Vatican", + "VE:VE-A": "Venezuela - Capital", + "VE:VE-B": "Venezuela - Anzoátegui", + "VE:VE-C": "Venezuela - Apure", + "VE:VE-D": "Venezuela - Aragua", + "VE:VE-E": "Venezuela - Barinas", + "VE:VE-F": "Venezuela - Bolívar", + "VE:VE-G": "Venezuela - Carabobo", + "VE:VE-H": "Venezuela - Cojedes", + "VE:VE-I": "Venezuela - Falcón", + "VE:VE-J": "Venezuela - Guárico", + "VE:VE-K": "Venezuela - Lara", + "VE:VE-L": "Venezuela - Mérida", + "VE:VE-M": "Venezuela - Miranda", + "VE:VE-N": "Venezuela - Monagas", + "VE:VE-O": "Venezuela - Nueva Esparta", + "VE:VE-P": "Venezuela - Portuguesa", + "VE:VE-R": "Venezuela - Sucre", + "VE:VE-S": "Venezuela - Táchira", + "VE:VE-T": "Venezuela - Trujillo", + "VE:VE-U": "Venezuela - Yaracuy", + "VE:VE-V": "Venezuela - Zulia", + "VE:VE-W": "Venezuela - Federal Dependencies", + "VE:VE-X": "Venezuela - La Guaira (Vargas)", + "VE:VE-Y": "Venezuela - Delta Amacuro", + "VE:VE-Z": "Venezuela - Amazonas", + "VN": "Vietnam", + "VG": "Virgin Islands (British)", + "VI": "Virgin Islands (US)", + "WF": "Wallis and Futuna", + "EH": "Western Sahara", + "YE": "Yemen", + "ZM:ZM-01": "Zambia - Western", + "ZM:ZM-02": "Zambia - Central", + "ZM:ZM-03": "Zambia - Eastern", + "ZM:ZM-04": "Zambia - Luapula", + "ZM:ZM-05": "Zambia - Northern", + "ZM:ZM-06": "Zambia - North-Western", + "ZM:ZM-07": "Zambia - Southern", + "ZM:ZM-08": "Zambia - Copperbelt", + "ZM:ZM-09": "Zambia - Lusaka", + "ZM:ZM-10": "Zambia - Muchinga", + "ZW": "Zimbabwe" +}; + +const countries = { + "AF": "Afghanistan", + "AX": "Åland Islands", + "AL": "Albania", + "DZ": "Algeria", + "AS": "American Samoa", + "AD": "Andorra", + "AO": "Angola", + "AI": "Anguilla", + "AQ": "Antarctica", + "AG": "Antigua and Barbuda", + "AR": "Argentina", + "AM": "Armenia", + "AW": "Aruba", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BS": "Bahamas", + "BH": "Bahrain", + "BD": "Bangladesh", + "BB": "Barbados", + "BY": "Belarus", + "PW": "Belau", + "BE": "Belgium", + "BZ": "Belize", + "BJ": "Benin", + "BM": "Bermuda", + "BT": "Bhutan", + "BO": "Bolivia", + "BQ": "Bonaire, Saint Eustatius and Saba", + "BA": "Bosnia and Herzegovina", + "BW": "Botswana", + "BV": "Bouvet Island", + "BR": "Brazil", + "IO": "British Indian Ocean Territory", + "BN": "Brunei", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA": "Canada", + "CV": "Cape Verde", + "KY": "Cayman Islands", + "CF": "Central African Republic", + "TD": "Chad", + "CL": "Chile", + "CN": "China", + "CX": "Christmas Island", + "CC": "Cocos (Keeling) Islands", + "CO": "Colombia", + "KM": "Comoros", + "CG": "Congo (Brazzaville)", + "CD": "Congo (Kinshasa)", + "CK": "Cook Islands", + "CR": "Costa Rica", + "HR": "Croatia", + "CU": "Cuba", + "CW": "Curaçao", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DK": "Denmark", + "DJ": "Djibouti", + "DM": "Dominica", + "DO": "Dominican Republic", + "EC": "Ecuador", + "EG": "Egypt", + "SV": "El Salvador", + "GQ": "Equatorial Guinea", + "ER": "Eritrea", + "EE": "Estonia", + "SZ": "Eswatini", + "ET": "Ethiopia", + "FK": "Falkland Islands", + "FO": "Faroe Islands", + "FJ": "Fiji", + "FI": "Finland", + "FR": "France", + "GF": "French Guiana", + "PF": "French Polynesia", + "TF": "French Southern Territories", + "GA": "Gabon", + "GM": "Gambia", + "GE": "Georgia", + "DE": "Germany", + "GH": "Ghana", + "GI": "Gibraltar", + "GR": "Greece", + "GL": "Greenland", + "GD": "Grenada", + "GP": "Guadeloupe", + "GU": "Guam", + "GT": "Guatemala", + "GG": "Guernsey", + "GN": "Guinea", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HT": "Haiti", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HK": "Hong Kong", + "HU": "Hungary", + "IS": "Iceland", + "IN": "India", + "ID": "Indonesia", + "IR": "Iran", + "IQ": "Iraq", + "IE": "Ireland", + "IM": "Isle of Man", + "IL": "Israel", + "IT": "Italy", + "CI": "Ivory Coast", + "JM": "Jamaica", + "JP": "Japan", + "JE": "Jersey", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KI": "Kiribati", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LA": "Laos", + "LV": "Latvia", + "LB": "Lebanon", + "LS": "Lesotho", + "LR": "Liberia", + "LY": "Libya", + "LI": "Liechtenstein", + "LT": "Lithuania", + "LU": "Luxembourg", + "MO": "Macao", + "MG": "Madagascar", + "MW": "Malawi", + "MY": "Malaysia", + "MV": "Maldives", + "ML": "Mali", + "MT": "Malta", + "MH": "Marshall Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MU": "Mauritius", + "YT": "Mayotte", + "MX": "Mexico", + "FM": "Micronesia", + "MD": "Moldova", + "MC": "Monaco", + "MN": "Mongolia", + "ME": "Montenegro", + "MS": "Montserrat", + "MA": "Morocco", + "MZ": "Mozambique", + "MM": "Myanmar", + "NA": "Namibia", + "NR": "Nauru", + "NP": "Nepal", + "NL": "Netherlands", + "NC": "New Caledonia", + "NZ": "New Zealand", + "NI": "Nicaragua", + "NE": "Niger", + "NG": "Nigeria", + "NU": "Niue", + "NF": "Norfolk Island", + "KP": "North Korea", + "MK": "North Macedonia", + "MP": "Northern Mariana Islands", + "NO": "Norway", + "OM": "Oman", + "PK": "Pakistan", + "PS": "Palestinian Territory", + "PA": "Panama", + "PG": "Papua New Guinea", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippines", + "PN": "Pitcairn", + "PL": "Poland", + "PT": "Portugal", + "PR": "Puerto Rico", + "QA": "Qatar", + "RE": "Reunion", + "RO": "Romania", + "RU": "Russia", + "RW": "Rwanda", + "ST": "São Tomé and Príncipe", + "BL": "Saint Barthélemy", + "SH": "Saint Helena", + "KN": "Saint Kitts and Nevis", + "LC": "Saint Lucia", + "SX": "Saint Martin (Dutch part)", + "MF": "Saint Martin (French part)", + "PM": "Saint Pierre and Miquelon", + "VC": "Saint Vincent and the Grenadines", + "WS": "Samoa", + "SM": "San Marino", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS": "Serbia", + "SC": "Seychelles", + "SL": "Sierra Leone", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "SB": "Solomon Islands", + "SO": "Somalia", + "ZA": "South Africa", + "GS": "South Georgia/Sandwich Islands", + "KR": "South Korea", + "SS": "South Sudan", + "ES": "Spain", + "LK": "Sri Lanka", + "SD": "Sudan", + "SR": "Suriname", + "SJ": "Svalbard and Jan Mayen", + "SE": "Sweden", + "CH": "Switzerland", + "SY": "Syria", + "TW": "Taiwan", + "TJ": "Tajikistan", + "TZ": "Tanzania", + "TH": "Thailand", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad and Tobago", + "TN": "Tunisia", + "TR": "Turkey", + "TM": "Turkmenistan", + "TC": "Turks and Caicos Islands", + "TV": "Tuvalu", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "United Arab Emirates", + "GB": "United Kingdom (UK)", + "US": "United States (US)", + "UM": "United States (US) Minor Outlying Islands", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VU": "Vanuatu", + "VA": "Vatican", + "VE": "Venezuela", + "VN": "Vietnam", + "VG": "Virgin Islands (British)", + "VI": "Virgin Islands (US)", + "WF": "Wallis and Futuna", + "EH": "Western Sahara", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe" +}; + +module.exports = { + countries, + currencies, + stateOptions, +}; diff --git a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js new file mode 100644 index 00000000000..8698cf8a017 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js @@ -0,0 +1,2574 @@ +const { + test, + expect +} = require('@playwright/test'); +const { + countries, + currencies, + stateOptions +} = require('../../data/settings'); + +/** + * Tests for the WooCommerce API. + * + * @group api + * @group settings + * + */ +test.describe('Settings API tests: CRUD', () => { + + test.describe('List all settings groups', () => { + + test('can retrieve all settings groups', async ({ + request + }) => { + // call API to retrieve all settings groups + const response = await request.get('/wp-json/wc/v3/settings'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "wc_admin", + label: "WooCommerce Admin", + description: "Settings for WooCommerce admin reporting.", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "general", + label: "General", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "products", + label: "Products", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "tax", + label: "Tax", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "shipping", + label: "Shipping", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "checkout", + label: "Payments", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "account", + label: "Accounts & Privacy", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email", + label: "Emails", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining(["email_new_order"]), + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "integration", + label: "Integration", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "advanced", + label: "Advanced", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_new_order", + label: "New order", + description: "New order emails are sent to chosen recipient(s) when a new order is received.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_cancelled_order", + label: "Cancelled order", + description: "Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_failed_order", + label: "Failed order", + description: "Failed order emails are sent to chosen recipient(s) when orders have been marked failed (if they were previously pending or on-hold).", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_on_hold_order", + label: "Order on-hold", + description: "This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Cancelled or Failed order status.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_processing_order", + label: "Processing order", + description: "This is an order notification sent to customers containing order details after payment.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_completed_order", + label: "Completed order", + description: "Order complete emails are sent to customers when their orders are marked completed and usually indicate that their orders have been shipped.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_refunded_order", + label: "Refunded order", + description: "Order refunded emails are sent to customers when their orders are refunded.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_invoice", + label: "Customer invoice / Order details", + description: "Customer invoice emails can be sent to customers containing their order information and payment links.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_note", + label: "Customer note", + description: "Customer note emails are sent when you add a note to an order.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_reset_password", + label: "Reset password", + description: "Customer \"reset password\" emails are sent when customers reset their passwords.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_new_account", + label: "New account", + description: "Customer \"new account\" emails are sent to the customer when a customer signs up via checkout or account pages.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + }); + }); + + + test.describe('List all settings options', () => { + + test('can retrieve all general settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/general'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_address", + label: "Address line 1", + description: "The street address for your business location.", + type: "text", + default: "", + tip: "The street address for your business location.", + value: "" + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_address_2", + label: "Address line 2", + description: "An additional, optional address line for your business location.", + type: "text", + default: "", + tip: "An additional, optional address line for your business location.", + value: "" + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_city", + label: "City", + description: "The city in which your business is located.", + type: "text", + default: "", + tip: "The city in which your business is located.", + value: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_default_country", + label: "Country / State", + description: "The country and state or province, if any, in which your business is located.", + type: "select", + default: "US:CA", + tip: "The country and state or province, if any, in which your business is located.", + value: "US:CA", + options: expect.objectContaining(stateOptions) + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_postcode", + label: "Postcode / ZIP", + description: "The postal code, if any, in which your business is located.", + type: "text", + default: "", + tip: "The postal code, if any, in which your business is located.", + value: "", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_allowed_countries", + label: "Selling location(s)", + description: "This option lets you limit which countries you are willing to sell to.", + type: "select", + default: "all", + tip: "This option lets you limit which countries you are willing to sell to.", + value: "all", + options: { + "all": "Sell to all countries", + "all_except": "Sell to all countries, except for…", + "specific": "Sell to specific countries" + }, + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_all_except_countries", + label: "Sell to all countries, except for…", + description: "", + type: "multiselect", + default: "", + value: "", + options: expect.objectContaining(countries), + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_specific_allowed_countries", + label: "Sell to specific countries", + description: "", + type: "multiselect", + default: "", + value: "", + options: expect.objectContaining(countries) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_ship_to_countries", + label: "Shipping location(s)", + description: "Choose which countries you want to ship to, or choose to ship to all locations you sell to.", + type: "select", + default: "", + tip: "Choose which countries you want to ship to, or choose to ship to all locations you sell to.", + value: "", + options: expect.objectContaining({ + "": "Ship to all countries you sell to", + "all": "Ship to all countries", + "specific": "Ship to specific countries only", + "disabled": "Disable shipping & shipping calculations" + }) + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_specific_ship_to_countries", + label: "Ship to specific countries", + description: "", + type: "multiselect", + default: "", + value: "", + options: expect.objectContaining(countries) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_default_customer_address", + label: "Default customer location", + description: "", + type: "select", + default: "base", + tip: "This option determines a customers default location. The MaxMind GeoLite Database will be periodically downloaded to your wp-content directory if using geolocation.", + value: "base", + options: expect.objectContaining({ + "": "No location by default", + "base": "Shop country/region", + "geolocation": "Geolocate", + "geolocation_ajax": "Geolocate (with page caching support)" + }) + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_calc_taxes", + label: "Enable taxes", + description: "Enable tax rates and calculations", + type: "checkbox", + default: "no", + tip: "Rates will be configurable and taxes will be calculated during checkout.", + value: expect.any(String), + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_enable_coupons", + label: "Enable coupons", + description: "Enable the use of coupon codes", + type: "checkbox", + default: "yes", + tip: "Coupons can be applied from the cart and checkout pages.", + value: "yes", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_calc_discounts_sequentially", + label: "", + description: "Calculate coupon discounts sequentially", + type: "checkbox", + default: "no", + tip: "When applying multiple coupons, apply the first coupon to the full price and the second coupon to the discounted price and so on.", + value: "no", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_currency", + label: "Currency", + description: "This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.", + type: "select", + default: "USD", + options: expect.objectContaining(currencies), + tip: "This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.", + value: "USD", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_currency_pos", + label: "Currency position", + description: "This controls the position of the currency symbol.", + type: "select", + default: "left", + options: { + "left": "Left", + "right": "Right", + "left_space": "Left with space", + "right_space": "Right with space" + }, + tip: "This controls the position of the currency symbol.", + value: "left", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_price_thousand_sep", + label: "Thousand separator", + description: "This sets the thousand separator of displayed prices.", + type: "text", + default: ",", + tip: "This sets the thousand separator of displayed prices.", + value: ",", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_price_decimal_sep", + label: "Decimal separator", + description: "This sets the decimal separator of displayed prices.", + type: "text", + default: ".", + tip: "This sets the decimal separator of displayed prices.", + value: ".", + }) + ])); + }); + }); + + test.describe('Retrieve a settings option', () => { + + test('can retrieve a settings option', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/general/woocommerce_allowed_countries'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "woocommerce_allowed_countries", + "label": "Selling location(s)", + "description": "This option lets you limit which countries you are willing to sell to.", + "type": "select", + "default": "all", + "options": { + "all": "Sell to all countries", + "all_except": "Sell to all countries, except for…", + "specific": "Sell to specific countries" + }, + "tip": "This option lets you limit which countries you are willing to sell to.", + "value": "all", + "group_id": "general", + }) + ); + + }); + + }); + + test.describe('Update a settings option', () => { + + test('can update a settings option', async ({ + request + }) => { + // call API to update settings options + const response = await request.put('/wp-json/wc/v3/settings/general/woocommerce_allowed_countries', { + data: { + value: "all_except" + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "woocommerce_allowed_countries", + "label": "Selling location(s)", + "description": "This option lets you limit which countries you are willing to sell to.", + "type": "select", + "default": "all", + "options": { + "all": "Sell to all countries", + "all_except": "Sell to all countries, except for…", + "specific": "Sell to specific countries" + }, + "tip": "This option lets you limit which countries you are willing to sell to.", + "value": "all_except", + "group_id": "general", + }) + ); + + }); + + }); + + test.describe('Batch Update a settings option', () => { + + test('can batch update settings options', async ({ + request + }) => { + + // call API to update settings options + const response = await request.post('/wp-json/wc/v3/settings/general/batch', { + data: { + update: [{ + id: "woocommerce_allowed_countries", + value: "all_except" + }, + { + id: "woocommerce_currency", + value: "GBP" + } + ] + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + // retrieve the updated settings values + const countriesUpdatedResponse = await request.get('/wp-json/wc/v3/settings/general/woocommerce_allowed_countries'); + const countriesUpdatedResponseJSON = await countriesUpdatedResponse.json(); + expect(countriesUpdatedResponseJSON.value).toEqual('all_except'); + + const currencyUpdatedResponse = await request.get('/wp-json/wc/v3/settings/general/woocommerce_currency'); + const currencyUpdatedResponseJSON = await currencyUpdatedResponse.json(); + expect(currencyUpdatedResponseJSON.value).toEqual('GBP'); + + // call API to restore the settings options + await request.put('/wp-json/wc/v3/settings/general/batch', { + data: { + update: [{ + id: "woocommerce_allowed_countries", + value: "all" + }, + { + id: "woocommerce_currency", + value: "USD" + } + ] + } + }); + }); + + }); + + test.describe('List all Products settings options', () => { + + test('can retrieve all products settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/products'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shop_page_id", + "label": "Shop page", + "type": "select", + "default": "", + "tip": "This sets the base page of your shop - this is where your product archive will be.", + "value": "5", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + }, + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_cart_redirect_after_add", + "label": "Add to cart behaviour", + "description": "Redirect to the cart page after successful addition", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_ajax_add_to_cart", + "label": "", + "description": "Enable AJAX add to cart buttons on archives", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_placeholder_image", + "label": "Placeholder image", + "description": "", + "type": "text", + "default": "", + "tip": "This is the attachment ID, or image URL, used for placeholder images in the product catalog. Products with no image will use this.", + "value": "4", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_weight_unit", + "label": "Weight unit", + "description": "This controls what unit you will define weights in.", + "type": "select", + "default": "kg", + "options": { + "kg": "kg", + "g": "g", + "lbs": "lbs", + "oz": "oz" + }, + "tip": "This controls what unit you will define weights in.", + "value": "kg", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_dimension_unit", + "label": "Dimensions unit", + "description": "This controls what unit you will define lengths in.", + "type": "select", + "default": "cm", + "options": { + "m": "m", + "cm": "cm", + "mm": "mm", + "in": "in", + "yd": "yd" + }, + "tip": "This controls what unit you will define lengths in.", + "value": "cm", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_reviews", + "label": "Enable reviews", + "description": "Enable product reviews", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_review_rating_verification_label", + "label": "", + "description": "Show \"verified owner\" label on customer reviews", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_review_rating_verification_required", + "label": "", + "description": "Reviews can only be left by \"verified owners\"", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_review_rating", + "label": "Product ratings", + "description": "Enable star rating on reviews", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_review_rating_required", + "label": "", + "description": "Star ratings should be required, not optional", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_manage_stock", + "label": "Manage stock", + "description": "Enable stock management", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_hold_stock_minutes", + "label": "Hold stock (minutes)", + "description": "Hold stock (for unpaid orders) for x minutes. When this limit is reached, the pending order will be cancelled. Leave blank to disable.", + "type": "number", + "default": "60", + "value": "60", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_low_stock", + "label": "Notifications", + "description": "Enable low stock notifications", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_no_stock", + "label": "", + "description": "Enable out of stock notifications", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_stock_email_recipient", + "label": "Notification recipient(s)", + "description": "Enter recipients (comma separated) that will receive this notification.", + "type": "text", + "default": "wordpress@example.com", + "tip": "Enter recipients (comma separated) that will receive this notification.", + "value": "wordpress@example.com", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_low_stock_amount", + "label": "Low stock threshold", + "description": "When product stock reaches this amount you will be notified via email.", + "type": "number", + "default": "2", + "tip": "When product stock reaches this amount you will be notified via email.", + "value": "2", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_no_stock_amount", + "label": "Out of stock threshold", + "description": "When product stock reaches this amount the stock status will change to \"out of stock\" and you will be notified via email. This setting does not affect existing \"in stock\" products.", + "type": "number", + "default": "0", + "tip": "When product stock reaches this amount the stock status will change to \"out of stock\" and you will be notified via email. This setting does not affect existing \"in stock\" products.", + "value": "0", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_hide_out_of_stock_items", + "label": "Out of stock visibility", + "description": "Hide out of stock items from the catalog", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_stock_format", + "label": "Stock display format", + "description": "This controls how stock quantities are displayed on the frontend.", + "type": "select", + "default": "", + "options": { + "": "Always show quantity remaining in stock e.g. \"12 in stock\"", + "low_amount": "Only show quantity remaining in stock when low e.g. \"Only 2 left in stock\"", + "no_amount": "Never show quantity remaining in stock" + }, + "tip": "This controls how stock quantities are displayed on the frontend.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_file_download_method", + "label": "File download method", + "description": "If you are using X-Accel-Redirect download method along with NGINX server, make sure that you have applied settings as described in Digital/Downloadable Product Handling guide.", + "type": "select", + "default": "force", + "options": { + "force": "Force downloads", + "xsendfile": "X-Accel-Redirect/X-Sendfile", + "redirect": "Redirect only (Insecure)" + }, + "tip": "Forcing downloads will keep URLs hidden, but some servers may serve large files unreliably. If supported, X-Accel-Redirect / X-Sendfile can be used to serve downloads instead (server requires mod_xsendfile).", + "value": "force", + }) + ])); + + + }); + }); + + test.describe('List all Tax settings options', () => { + + test('can retrieve all tax settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/tax'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_prices_include_tax", + "label": "Prices entered with tax", + "description": "", + "type": "radio", + "default": "no", + "options": { + "yes": "Yes, I will enter prices inclusive of tax", + "no": "No, I will enter prices exclusive of tax" + }, + "tip": "This option is important as it will affect how you input prices. Changing it will not update existing products.", + "value": "no", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_tax_based_on", + "label": "Calculate tax based on", + "description": "", + "type": "select", + "default": "shipping", + "options": { + "shipping": "Customer shipping address", + "billing": "Customer billing address", + "base": "Shop base address" + }, + "tip": "This option determines which address is used to calculate tax.", + "value": "shipping", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_tax_class", + "label": "Shipping tax class", + "description": "Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.", + "type": "select", + "default": "inherit", + "options": { + "inherit": "Shipping tax class based on cart items", + "": "Standard", + "reduced-rate": "Reduced rate", + "zero-rate": "Zero rate" + }, + "tip": "Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.", + "value": "inherit", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_tax_round_at_subtotal", + "label": "Rounding", + "description": "Round tax at subtotal level, instead of rounding per line", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_tax_classes", + "label": "Additional tax classes", + "description": "", + "type": "textarea", + "default": "", + "tip": "List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to \"Standard rate\" which exists by default.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_tax_display_shop", + "label": "Display prices in the shop", + "description": "", + "type": "select", + "default": "excl", + "options": { + "incl": "Including tax", + "excl": "Excluding tax" + }, + "value": "excl", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_tax_display_cart", + "label": "Display prices during cart and checkout", + "description": "", + "type": "select", + "default": "excl", + "options": { + "incl": "Including tax", + "excl": "Excluding tax" + }, + "value": "excl", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_price_display_suffix", + "label": "Price display suffix", + "description": "", + "type": "text", + "default": "", + "tip": "Define text to show after your product prices. This could be, for example, \"inc. Vat\" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_tax_total_display", + "label": "Display tax totals", + "description": "", + "type": "select", + "default": "itemized", + "options": { + "single": "As a single total", + "itemized": "Itemized" + }, + "value": "itemized", + }) + ])); + + + + }); + }); + + test.describe('List all Shipping settings options', () => { + + test('can retrieve all shipping settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/shipping'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_shipping_calc", + "label": "Calculations", + "description": "Enable the shipping calculator on the cart page", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_cost_requires_address", + "label": "", + "description": "Hide shipping costs until an address is entered", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_ship_to_destination", + "label": "Shipping destination", + "description": "This controls which shipping address is used by default.", + "type": "radio", + "default": "billing", + "options": { + "shipping": "Default to customer shipping address", + "billing": "Default to customer billing address", + "billing_only": "Force shipping to the customer billing address" + }, + "tip": "This controls which shipping address is used by default.", + "value": "billing", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_debug_mode", + "label": "Debug mode", + "description": "Enable debug mode", + "type": "checkbox", + "default": "no", + "tip": "Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_shipping_calc", + "label": "Calculations", + "description": "Enable the shipping calculator on the cart page", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_cost_requires_address", + "label": "", + "description": "Hide shipping costs until an address is entered", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_ship_to_destination", + "label": "Shipping destination", + "description": "This controls which shipping address is used by default.", + "type": "radio", + "default": "billing", + "options": { + "shipping": "Default to customer shipping address", + "billing": "Default to customer billing address", + "billing_only": "Force shipping to the customer billing address" + }, + "tip": "This controls which shipping address is used by default.", + "value": "billing", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_debug_mode", + "label": "Debug mode", + "description": "Enable debug mode", + "type": "checkbox", + "default": "no", + "tip": "Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.", + "value": "no", + }) + ])); + + }); + }); + + test.describe('List all Checkout settings options', () => { + + test('can retrieve all checkout settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/checkout'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([])); + }); + }); + + test.describe('List all Account settings options', () => { + + test('can retrieve all account settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/account'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_guest_checkout", + "label": "Guest checkout", + "description": "Allow customers to place orders without an account", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_checkout_login_reminder", + "label": "Login", + "description": "Allow customers to log into an existing account during checkout", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_signup_and_login_from_checkout", + "label": "Account creation", + "description": "Allow customers to create an account during checkout", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_myaccount_registration", + "label": "", + "description": "Allow customers to create an account on the \"My account\" page", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_registration_generate_username", + "label": "", + "description": "When creating an account, automatically generate an account username for the customer based on their name, surname or email", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_registration_generate_password", + "label": "", + "description": "When creating an account, send the new user a link to set their password", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_erasure_request_removes_order_data", + "label": "Account erasure requests", + "description": "Remove personal data from orders on request", + "type": "checkbox", + "default": "no", + "tip": expect.stringContaining('When handling an account erasure request, should personal data within orders be retained or removed?'), + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_erasure_request_removes_download_data", + "label": "", + "description": "Remove access to downloads on request", + "type": "checkbox", + "default": "no", + "tip": expect.stringContaining('When handling an account erasure request, should access to downloadable files be revoked and download logs cleared?'), + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_allow_bulk_remove_personal_data", + "label": "Personal data removal", + "description": "Allow personal data to be removed in bulk from orders", + "type": "checkbox", + "default": "no", + "tip": "Adds an option to the orders screen for removing personal data in bulk. Note that removing personal data cannot be undone.", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_registration_privacy_policy_text", + "label": "Registration privacy policy", + "description": "", + "type": "textarea", + "default": "Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our [privacy_policy].", + "tip": "Optionally add some text about your store privacy policy to show on account registration forms.", + "value": "Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our [privacy_policy].", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_checkout_privacy_policy_text", + "label": "Checkout privacy policy", + "description": "", + "type": "textarea", + "default": "Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our [privacy_policy].", + "tip": "Optionally add some text about your store privacy policy to show during checkout.", + "value": "Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our [privacy_policy].", + }) + ])); + + }); + }); + + test.describe('List all Email settings options', () => { + + test('can retrieve all email settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_from_name", + "label": "\"From\" name", + "description": "How the sender name appears in outgoing WooCommerce emails.", + "type": "text", + "default": "WooCommerce Core E2E Test Suite", + "tip": "How the sender name appears in outgoing WooCommerce emails.", + "value": "woocommerce", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_from_address", + "label": "\"From\" address", + "description": "How the sender email appears in outgoing WooCommerce emails.", + "type": "email", + "default": "wordpress@example.com", + "tip": "How the sender email appears in outgoing WooCommerce emails.", + "value": "wordpress@example.com", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_header_image", + "label": "Header image", + "description": "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).", + "type": "text", + "default": "", + "tip": "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_footer_text", + "label": "Footer text", + "description": "The text to appear in the footer of all WooCommerce emails. Available placeholders: {site_title} {site_url}", + "type": "textarea", + "default": "{site_title} — Built with {WooCommerce}", + "tip": "The text to appear in the footer of all WooCommerce emails. Available placeholders: {site_title} {site_url}", + "value": "{site_title} — Built with {WooCommerce}", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_base_color", + "label": "Base color", + "description": "The base color for WooCommerce email templates. Default #7f54b3.", + "type": "color", + "default": "#7f54b3", + "tip": "The base color for WooCommerce email templates. Default #7f54b3.", + "value": "#7f54b3", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_background_color", + "label": "Background color", + "description": "The background color for WooCommerce email templates. Default #f7f7f7.", + "type": "color", + "default": "#f7f7f7", + "tip": "The background color for WooCommerce email templates. Default #f7f7f7.", + "value": "#f7f7f7", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_body_background_color", + "label": "Body background color", + "description": "The main body background color. Default #ffffff.", + "type": "color", + "default": "#ffffff", + "tip": "The main body background color. Default #ffffff.", + "value": "#ffffff", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_text_color", + "label": "Body text color", + "description": "The main body text color. Default #3c3c3c.", + "type": "color", + "default": "#3c3c3c", + "tip": "The main body text color. Default #3c3c3c.", + "value": "#3c3c3c", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_merchant_email_notifications", + "label": "Enable email insights", + "description": "Receive email notifications with additional guidance to complete the basic store setup and helpful insights", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + + + }); + }); + + test.describe('List all Advanced settings options', () => { + + test('can retrieve all advanced settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/advanced'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_cart_page_id", + "label": "Cart page", + "description": "Page contents: [woocommerce_cart]", + "type": "select", + "default": "", + "tip": "Page contents: [woocommerce_cart]", + "value": "6", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + }, + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_checkout_page_id", + "label": "Checkout page", + "description": "Page contents: [woocommerce_checkout]", + "type": "select", + "default": 7, + "tip": "Page contents: [woocommerce_checkout]", + "value": "7", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + } + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_page_id", + "label": "My account page", + "description": "Page contents: [woocommerce_my_account]", + "type": "select", + "default": "", + "tip": "Page contents: [woocommerce_my_account]", + "value": "8", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + }, + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_checkout_pay_endpoint", + "label": "Pay", + "description": "Endpoint for the \"Checkout → Pay\" page.", + "type": "text", + "default": "order-pay", + "tip": "Endpoint for the \"Checkout → Pay\" page.", + "value": "order-pay", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_checkout_order_received_endpoint", + "label": "Order received", + "description": "Endpoint for the \"Checkout → Order received\" page.", + "type": "text", + "default": "order-received", + "tip": "Endpoint for the \"Checkout → Order received\" page.", + "value": "order-received", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_add_payment_method_endpoint", + "label": "Add payment method", + "description": "Endpoint for the \"Checkout → Add payment method\" page.", + "type": "text", + "default": "add-payment-method", + "tip": "Endpoint for the \"Checkout → Add payment method\" page.", + "value": "add-payment-method", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_delete_payment_method_endpoint", + "label": "Delete payment method", + "description": "Endpoint for the delete payment method page.", + "type": "text", + "default": "delete-payment-method", + "tip": "Endpoint for the delete payment method page.", + "value": "delete-payment-method", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_orders_endpoint", + "label": "Orders", + "description": "Endpoint for the \"My account → Orders\" page.", + "type": "text", + "default": "orders", + "tip": "Endpoint for the \"My account → Orders\" page.", + "value": "orders", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_view_order_endpoint", + "label": "View order", + "description": "Endpoint for the \"My account → View order\" page.", + "type": "text", + "default": "view-order", + "tip": "Endpoint for the \"My account → View order\" page.", + "value": "view-order", + }) + ])); + + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_downloads_endpoint", + "label": "Downloads", + "description": "Endpoint for the \"My account → Downloads\" page.", + "type": "text", + "default": "downloads", + "tip": "Endpoint for the \"My account → Downloads\" page.", + "value": "downloads", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_edit_account_endpoint", + "label": "Edit account", + "description": "Endpoint for the \"My account → Edit account\" page.", + "type": "text", + "default": "edit-account", + "tip": "Endpoint for the \"My account → Edit account\" page.", + "value": "edit-account", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_edit_address_endpoint", + "label": "Addresses", + "description": "Endpoint for the \"My account → Addresses\" page.", + "type": "text", + "default": "edit-address", + "tip": "Endpoint for the \"My account → Addresses\" page.", + "value": "edit-address", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_payment_methods_endpoint", + "label": "Payment methods", + "description": "Endpoint for the \"My account → Payment methods\" page.", + "type": "text", + "default": "payment-methods", + "tip": "Endpoint for the \"My account → Payment methods\" page.", + "value": "payment-methods", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_myaccount_lost_password_endpoint", + "label": "Lost password", + "description": "Endpoint for the \"My account → Lost password\" page.", + "type": "text", + "default": "lost-password", + "tip": "Endpoint for the \"My account → Lost password\" page.", + "value": "lost-password", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_logout_endpoint", + "label": "Logout", + "description": "Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true", + "type": "text", + "default": "customer-logout", + "tip": "Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true", + "value": "customer-logout", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_api_enabled", + "label": "Legacy API", + "description": "Enable the legacy REST API", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_allow_tracking", + "label": "Enable tracking", + "description": "Allow usage of WooCommerce to be tracked", + "type": "checkbox", + "default": "no", + "tip": "To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at: WooCommerce.com Usage Tracking Documentation.", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_show_marketplace_suggestions", + "label": "Show Suggestions", + "description": "Display suggestions within WooCommerce", + "type": "checkbox", + "default": "yes", + "tip": "Leave this box unchecked if you do not want to see suggested extensions.", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_analytics_enabled", + "label": "Analytics", + "description": "Enables WooCommerce Analytics", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_multichannel_marketing_enabled", + "label": "Marketing", + "description": "Enables the new WooCommerce Multichannel Marketing experience in the Marketing page", + "type": "checkbox", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_navigation_enabled", + "label": "Navigation", + "description": "Adds the new WooCommerce navigation experience to the dashboard", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + + }); + }); + + + test.describe('List all Email New Order settings', () => { + + test('can retrieve all email new order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_new_order'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "recipient", + "label": "Recipient(s)", + "description": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "type": "text", + "default": "", + "tip": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "type": "textarea", + "default": "Congratulations on the sale.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "value": "Congratulations on the sale.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Failed Order settings', () => { + + test('can retrieve all email failed order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_failed_order'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "recipient", + "label": "Recipient(s)", + "description": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "type": "text", + "default": "", + "tip": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "Hopefully they’ll be back. Read more about troubleshooting failed payments.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "Hopefully they’ll be back. Read more about troubleshooting failed payments.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer On Hold Order settings', () => { + + test('can retrieve all email customer on hold order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_on_hold_order'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "We look forward to fulfilling your order soon.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "We look forward to fulfilling your order soon.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer Processing Order settings', () => { + + test('can retrieve all email customer processsing order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_processing_order'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "Thanks for using {site_url}!", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "Thanks for using {site_url}!", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer Completed Order settings', () => { + + test('can retrieve all email customer completed order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_completed_order'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "Thanks for shopping with us.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "Thanks for shopping with us.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer Refunded Order settings', () => { + + test('can retrieve all email customer refunded order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_refunded_order'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject_full", + "label": "Full refund subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject_partial", + "label": "Partial refund subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading_full", + "label": "Full refund email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading_partial", + "label": "Partial refund email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "We hope to see you again soon.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "We hope to see you again soon.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer Invoice settings', () => { + + test('can retrieve all email customer invoice settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_invoice'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject_paid", + "label": "Subject (paid)", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading_paid", + "label": "Email heading (paid)", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "Thanks for using {site_url}!", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "Thanks for using {site_url}!", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer Note settings', () => { + + test('can retrieve all email customer note settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_note'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "type": "textarea", + "default": "Thanks for reading.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}", + "value": "Thanks for reading.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + + test.describe('List all Email Customer Reset Password settings', () => { + + test('can retrieve all email customer reset password settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_reset_password'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "textarea", + "default": "Thanks for reading.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "Thanks for reading.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + }); + }); + + test.describe('List all Email Customer New Account settings', () => { + + test('can retrieve all email customer new account settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_new_account'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "enabled", + "label": "Enable/Disable", + "description": "", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "additional_content", + "label": "Additional content", + "description": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "textarea", + "default": "We look forward to seeing you soon.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "We look forward to seeing you soon.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "email_type", + "label": "Email type", + "description": "Choose which format of email to send.", + "type": "select", + "default": "html", + "options": { + "plain": "Plain text", + "html": "HTML", + "multipart": "Multipart" + }, + "tip": "Choose which format of email to send.", + "value": "html", + }) + ])); + + }); + }); + +}); From afe3b4af01062c0b7a33db214444202f36fb2aaf Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Wed, 26 Oct 2022 11:14:38 -0500 Subject: [PATCH 079/149] Fix line endings in change file from 34292 (#35334) * Fix line endings in change file from 34292 * Correctly update change file --- .../changelog/improve-cat-dashboard-loading-time | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time b/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time index f0aed0679ee..8732f051e96 100644 --- a/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time +++ b/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time @@ -1,4 +1,4 @@ -Significance: patch -Type: update - -Improve the loading time of WooCommerce setup widget for large databases +Significance: patch +Type: update + +Improve the loading time of WooCommerce setup widget for large databases From 4cb9bc45c4c64f8aa2231d24926fb685d878510b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20L=C3=B3pez=20Ariza?= <45979455+alopezari@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:29:41 +0200 Subject: [PATCH 080/149] Updated COT plugin used to set up the local environment with COT enabled. (#34990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated COT plugin used to set up the local environment with COT enabled. * Enable HPOS in the e2e environment using env var (#35057) * Updated COT plugin used to set up the local environment with COT enabled. * Used env var to enable HPOS * Disable HPOS for performance tests * Updated permissions * Set ENABLE_HPOS to 1 Co-authored-by: Alex López Co-authored-by: Jamel Noel Reid --- .../cot-build-and-e2e-tests-daily.yml | 4 +++ .../workflows/cot-pr-build-and-e2e-tests.yml | 4 +++ .github/workflows/pr-build-and-e2e-tests.yml | 6 +++++ .../woocommerce/changelog/fix-cot-env-setup | 4 +++ plugins/woocommerce/package.json | 2 +- .../tests/e2e-pw/bin/test-env-setup.sh | 26 +++++-------------- .../performance/bin/init-sample-products.sh | 8 ++++++ 7 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-cot-env-setup diff --git a/.github/workflows/cot-build-and-e2e-tests-daily.yml b/.github/workflows/cot-build-and-e2e-tests-daily.yml index b4beda9e3fc..51cd250ce12 100644 --- a/.github/workflows/cot-build-and-e2e-tests-daily.yml +++ b/.github/workflows/cot-build-and-e2e-tests-daily.yml @@ -23,6 +23,8 @@ jobs: - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Download and install Chromium browser. @@ -75,6 +77,8 @@ jobs: - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Run Playwright API tests. diff --git a/.github/workflows/cot-pr-build-and-e2e-tests.yml b/.github/workflows/cot-pr-build-and-e2e-tests.yml index 1a1531deae8..e5e6e4bd4cc 100644 --- a/.github/workflows/cot-pr-build-and-e2e-tests.yml +++ b/.github/workflows/cot-pr-build-and-e2e-tests.yml @@ -24,6 +24,8 @@ jobs: - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Download and install Chromium browser. @@ -77,6 +79,8 @@ jobs: - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Run Playwright API tests. diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml index f6a4c8e3f47..a5f7a7afe31 100644 --- a/.github/workflows/pr-build-and-e2e-tests.yml +++ b/.github/workflows/pr-build-and-e2e-tests.yml @@ -24,6 +24,8 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 0 run: pnpm env:test --filter=woocommerce - name: Download and install Chromium browser. @@ -87,6 +89,8 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 0 run: pnpm env:test --filter=woocommerce - name: Run Playwright API tests. @@ -132,6 +136,8 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 0 run: | pnpm env:dev --filter=woocommerce pnpm env:performance-init --filter=woocommerce diff --git a/plugins/woocommerce/changelog/fix-cot-env-setup b/plugins/woocommerce/changelog/fix-cot-env-setup new file mode 100644 index 00000000000..7c977609bf4 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cot-env-setup @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Updated the COT plugin URL now that this feature can be enabled in a different way. diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index e56d993d17a..9149fffe73b 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -33,7 +33,7 @@ "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh", "e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", - "env:test:cot": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh --cot", + "env:test:cot": "pnpm run env:dev && ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh", "env:performance-init": "./tests/performance/bin/init-sample-products.sh", "env:down": "pnpm wp-env stop", "env:destroy": "pnpm wp-env destroy", diff --git a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh index fb02768209d..ebb2cac3099 100755 --- a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh +++ b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +ENABLE_HPOS="${ENABLE_HPOS:-0}" + wp-env run tests-cli "wp theme install twentynineteen --activate" wp-env run tests-cli "wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate" @@ -22,23 +24,7 @@ wp-env run tests-cli "wp user create customer customer@woocommercecoree2etestsui echo -e 'Update Blog Name \n' wp-env run tests-cli 'wp option update blogname "WooCommerce Core E2E Test Suite"' -# Enable additional WooCommerce features based on command options -while :; do - case $1 in - -c|--cot) # Enable the COT feature - echo 'Enable the COT feature' - wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/9d5f174ebf8eec8e0ce5417d00728524c7f3b6b3.zip --activate" - ;; - --) # End of all options - shift - break - ;; - -?*) - echo "WARN: Unknown option (ignored):" $1 >&2 - ;; - *) # No more options, so break out of the loop - break - esac - - shift -done +if [ $ENABLE_HPOS == 1 ]; then + echo 'Enable the COT feature' + wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/b031465052fc3e04b17624acbeeb2569ef4d5301.zip --activate" +fi diff --git a/plugins/woocommerce/tests/performance/bin/init-sample-products.sh b/plugins/woocommerce/tests/performance/bin/init-sample-products.sh index 8ff1244b972..e5a941876f4 100755 --- a/plugins/woocommerce/tests/performance/bin/init-sample-products.sh +++ b/plugins/woocommerce/tests/performance/bin/init-sample-products.sh @@ -1,5 +1,7 @@ #!/bin/bash +ENABLE_HPOS="${ENABLE_HPOS:-0}" + echo "Initializing WooCommerce E2E" wp-env run tests-cli "wp plugin activate woocommerce" @@ -40,3 +42,9 @@ wp-env run tests-cli "wp import wp-content/plugins/woocommerce/sample-data/sampl wp-env run tests-cli "wp theme install storefront --activate" echo "Success! Your E2E Test Environment is now ready." + + +if [ $ENABLE_HPOS == 1 ]; then + echo 'Enable the COT feature' + wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/b031465052fc3e04b17624acbeeb2569ef4d5301.zip --activate" +fi From 89961fe067d24868340c3a98157dc8f322f0da7f Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Wed, 26 Oct 2022 11:20:11 -0700 Subject: [PATCH 081/149] Add summary field to new product experience (#35201) * Add summary to new product page experience * Add changelog entry --- .../products/sections/product-details-section.tsx | 14 ++++++++++++++ plugins/woocommerce/changelog/add-35165 | 4 ++++ 2 files changed, 18 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-35165 diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index bf56bca4973..7f8bfd0b979 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -53,6 +53,9 @@ export const ProductDetailsSection: React.FC = () => { const [ descriptionBlocks, setDescriptionBlocks ] = useState< BlockInstance[] >( parse( values.description || '' ) ); + const [ summaryBlocks, setSummaryBlocks ] = useState< BlockInstance[] >( + parse( values.short_description || '' ) + ); const { permalinkPrefix, permalinkSuffix } = useSelect( ( select: WCDataSelector ) => { const { getPermalinkParts } = select( PRODUCTS_STORE_NAME ); @@ -205,6 +208,17 @@ export const ProductDetailsSection: React.FC = () => { } /> ) } + { + setSummaryBlocks( blocks ); + setValue( + 'short_description', + serialize( blocks ) + ); + } } + /> Date: Wed, 26 Oct 2022 12:07:57 -0700 Subject: [PATCH 082/149] Skip flakey settings API test. (#35338) Co-authored-by: Jon Lane --- plugins/woocommerce/changelog/fix-skip-failing-api-test | 4 ++++ .../tests/api-core-tests/tests/settings/settings-crud.test.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-skip-failing-api-test diff --git a/plugins/woocommerce/changelog/fix-skip-failing-api-test b/plugins/woocommerce/changelog/fix-skip-failing-api-test new file mode 100644 index 00000000000..86578e5f08d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-skip-failing-api-test @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Skip flaky settings API test diff --git a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js index 8698cf8a017..78853b7e380 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js @@ -239,7 +239,7 @@ test.describe('Settings API tests: CRUD', () => { test.describe('List all settings options', () => { - test('can retrieve all general settings', async ({ + test.fixme('can retrieve all general settings', async ({ request }) => { // call API to retrieve all settings options From bfe0e958b80a3268d601a3f9d72fe608f90f9a4a Mon Sep 17 00:00:00 2001 From: Jamel Noel Reid Date: Wed, 26 Oct 2022 14:36:44 -0500 Subject: [PATCH 083/149] Revert setting up permalinks in PW global setup (#35337) * Revert permalinks setup * Add changelog Co-authored-by: Jon Lane --- .../changelog/revert-setup-permalinks | 4 ++ .../woocommerce/tests/e2e-pw/global-setup.js | 37 ------------------- 2 files changed, 4 insertions(+), 37 deletions(-) create mode 100644 plugins/woocommerce/changelog/revert-setup-permalinks diff --git a/plugins/woocommerce/changelog/revert-setup-permalinks b/plugins/woocommerce/changelog/revert-setup-permalinks new file mode 100644 index 00000000000..5000ded536d --- /dev/null +++ b/plugins/woocommerce/changelog/revert-setup-permalinks @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Revert the changes introduced in PR #35282 diff --git a/plugins/woocommerce/tests/e2e-pw/global-setup.js b/plugins/woocommerce/tests/e2e-pw/global-setup.js index f443dd56cb6..988aabc36a5 100644 --- a/plugins/woocommerce/tests/e2e-pw/global-setup.js +++ b/plugins/woocommerce/tests/e2e-pw/global-setup.js @@ -11,19 +11,6 @@ const adminPassword = ADMIN_PASSWORD ?? 'password'; const customerUsername = CUSTOMER_USER ?? 'customer'; const customerPassword = CUSTOMER_PASSWORD ?? 'password'; -const setupPermalinks = async ( adminPage, baseURL ) => { - console.log( 'Trying to setup permalinks!' ); - await adminPage.goto( baseURL + '/wp-admin/options-permalink.php' ); - await expect( adminPage.locator( 'div.wrap > h1' ) ).toHaveText( - 'Permalink Settings' - ); - await adminPage.click( '#custom_selection' ); - await adminPage.fill( '#permalink_structure', '/%postname%/' ); - await adminPage.click( '#submit' ); - - console.log( 'Permalinks Set!' ); -} - module.exports = async ( config ) => { const { stateDir, baseURL, userAgent } = config.projects[ 0 ].use; @@ -142,30 +129,6 @@ module.exports = async ( config ) => { process.exit( 1 ); } - // Ensure that permalinks are correctly setup since we can't set it up using wp-env when not using the default WP version. - // More info here: https://github.com/WordPress/gutenberg/issues/28201 - let permalinkConfigured = false; - const permalinkRetries = 5; - for ( let i = 0; i < permalinkRetries; i++ ) { - try { - await setupPermalinks( adminPage, baseURL ); - permalinkConfigured = true; - break; - } catch ( e ) { - console.log( - `Setting permalink failed, Retrying... ${ i }/${ permalinkRetries }` - ); - console.log( e ); - } - } - - if ( ! permalinkConfigured ) { - console.error( - 'Cannot proceed e2e test, as we could not setup permalinks. Please check if the test site has been setup correctly.' - ); - process.exit( 1 ); - } - // Sign in as customer user and save state const customerRetries = 5; for ( let i = 0; i < customerRetries; i++ ) { From c539e5614fab45358b6641af2e6b4811b61205e4 Mon Sep 17 00:00:00 2001 From: Thomas Roberts <5656702+opr@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:28:44 +0100 Subject: [PATCH 084/149] Update `@woocommerce/extend-checkout-block` to include an example of a forced inner block (#33833) * Update main integration file to register newsletter block scripts/styles * Add files for example newsletter block * Use only one internal dependencies comment * Include newsletter block in index * Move entry to correct position in webpack * Rename functions to register scripts and styles * Add changelog * Remove block registration code * Register slug as a block category to prevent warnings in console * Add comment explaining the purpose of the function * Bring templates up to date with latest changes to WC Blocks The way the validation store selectors work was updated, so this template will use the new selectors. * Ensure error message shows when box is not checked * Ensure checkbox is targeted with CSS in editor --- .../$slug-blocks-integration.php.mustache | 84 +++++++++++++++++-- .../$slug.php.mustache | 16 ++++ .../changelog/add-inner-block-example | 4 + .../src/index.js.mustache | 3 +- .../block.js.mustache | 67 +++++++++++++++ .../block.json.mustache | 34 ++++++++ .../edit.js.mustache | 49 +++++++++++ .../frontend.js.mustache | 14 ++++ .../index.js.mustache | 33 ++++++++ .../style.scss.mustache | 27 ++++++ .../src/js/index.js.mustache | 7 +- .../webpack.config.js.mustache | 17 ++++ 12 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 packages/js/extend-cart-checkout-block/changelog/add-inner-block-example create mode 100644 packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache create mode 100644 packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache create mode 100644 packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache create mode 100644 packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache create mode 100644 packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache create mode 100644 packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache diff --git a/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache b/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache index 26e74cac439..89762e199dd 100644 --- a/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache +++ b/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache @@ -19,9 +19,18 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { /** * When called invokes any initialization/setup for the integration. - * */ public function initialize() { + $this->register_newsletter_block_frontend_scripts(); + $this->register_newsletter_block_editor_scripts(); + $this->register_newsletter_block_editor_styles(); + $this->register_main_integration(); + } + + /** + * Registers the main JS file required to add filters and Slot/Fills. + */ + public function register_main_integration() { $script_path = '/build/index.js'; $style_path = '/build/style-index.css'; @@ -53,7 +62,6 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { wp_set_script_translations( '{{slug}}-blocks-integration', '{{slug}}', - '{{slug}}', dirname( __FILE__ ) . '/languages' ); } @@ -64,7 +72,7 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { * @return string[] */ public function get_script_handles() { - return array( '{{slug}}-blocks-integration' ); + return array( '{{slug}}-blocks-integration', '{{slug}}-checkout-newsletter-subscription-block-frontend' ); } /** @@ -73,7 +81,7 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { * @return string[] */ public function get_editor_script_handles() { - return array( '{{slug}}-blocks-integration' ); + return array( '{{slug}}-blocks-integration', '{{slug}}-checkout-newsletter-subscription-block-editor' ); } /** @@ -84,13 +92,77 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { public function get_script_data() { $data = array( '{{slug}}-active' => true, - 'example-data' => 'This is some example data from the server', + 'example-data' => __( 'This is some example data from the server', '{{slug}}' ), + 'optInDefaultText' => __( 'I want to receive updates about products and promotions.', '{{ slug }}' ), ); return $data; } + public function register_newsletter_block_editor_styles() { + $style_path = '/build/style-{{slug}}-checkout-newsletter-subscription-block.css'; + + $style_url = plugins_url( $style_path, __FILE__ ); + wp_enqueue_style( + '{{slug}}-blocks-integration', + $style_url, + [], + $this->get_file_version( $style_path ) + ); + } + + public function register_newsletter_block_editor_scripts() { + $script_path = '/build/{{slug}}-checkout-newsletter-subscription-block.js'; + $script_url = plugins_url( $script_path, __FILE__ ); + $script_asset_path = dirname( __FILE__ ) . '/build/{{slug}}-checkout-newsletter-subscription-block.asset.php'; + $script_asset = file_exists( $script_asset_path ) + ? require $script_asset_path + : array( + 'dependencies' => array(), + 'version' => $this->get_file_version( $script_asset_path ), + ); + + wp_register_script( + '{{slug}}-checkout-newsletter-subscription-block-editor', + $script_url, + $script_asset['dependencies'], + $script_asset['version'], + true + ); + + wp_set_script_translations( + '{{slug}}-newsletter-block-editor', // script handle + '{{slug}}', // text domain + dirname( __FILE__ ) . '/languages' + ); + } + + public function register_newsletter_block_frontend_scripts() { + $script_path = '/build/{{slug}}-checkout-newsletter-subscription-block-frontend.js'; + $script_url = plugins_url( $script_path, __FILE__ ); + $script_asset_path = dirname( __FILE__ ) . '/build/newsletter-block-frontend.asset.php'; + $script_asset = file_exists( $script_asset_path ) + ? require $script_asset_path + : array( + 'dependencies' => array(), + 'version' => $this->get_file_version( $script_asset_path ), + ); + + wp_register_script( + '{{slug}}-checkout-newsletter-subscription-block-frontend', + $script_url, + $script_asset['dependencies'], + $script_asset['version'], + true + ); + wp_set_script_translations( + '{{slug}}-checkout-newsletter-subscription-block-frontend', // script handle + '{{slug}}', // text domain + dirname( __FILE__ ) . '/languages' + ); + } + /** * Get the file modified time as a cache buster if we're in dev mode. * @@ -103,4 +175,4 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { } return {{slugPascalCase}}_VERSION; } -} \ No newline at end of file +} diff --git a/packages/js/extend-cart-checkout-block/$slug.php.mustache b/packages/js/extend-cart-checkout-block/$slug.php.mustache index c9f5d74c578..efe1a073e49 100644 --- a/packages/js/extend-cart-checkout-block/$slug.php.mustache +++ b/packages/js/extend-cart-checkout-block/$slug.php.mustache @@ -33,3 +33,19 @@ add_action('woocommerce_blocks_loaded', function() { } ); }); + +/** + * Registers the slug as a block category with WordPress. + */ +function register_{{slugPascalCase}}_block_category( $categories ) { + return array_merge( + $categories, + [ + [ + 'slug' => '{{slug}}', + 'title' => __( '{{slugPascalCase}} Blocks', '{{slug}}' ), + ], + ] + ); +} +add_action( 'block_categories_all', 'register_{{slugPascalCase}}_block_category', 10, 2 ); diff --git a/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example b/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example new file mode 100644 index 00000000000..1a22ae4197f --- /dev/null +++ b/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Include an example of adding an inner block to the WooCommerce Blocks Checkout Block diff --git a/packages/js/extend-cart-checkout-block/src/index.js.mustache b/packages/js/extend-cart-checkout-block/src/index.js.mustache index fe131bcb220..b6da8429a0d 100644 --- a/packages/js/extend-cart-checkout-block/src/index.js.mustache +++ b/packages/js/extend-cart-checkout-block/src/index.js.mustache @@ -1 +1,2 @@ -import './js/index'; \ No newline at end of file +import './js/index'; +import './js/checkout-newsletter-subscription-block'; diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache new file mode 100644 index 00000000000..eab6fe1bf50 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { getSetting } from '@woocommerce/settings'; +import { useSelect, useDispatch } from '@wordpress/data'; + +const { optInDefaultText } = getSetting( '{{slug}}_data', '' ); + +const Block = ( { children, checkoutExtensionData } ) => { + const [ checked, setChecked ] = useState( false ); + const { setExtensionData } = checkoutExtensionData; + + const { setValidationErrors, clearValidationError } = useDispatch( + 'wc/store/validation' + ); + + useEffect( () => { + setExtensionData( '{{slug}}', 'optin', checked ); + if ( ! checked ) { + setValidationErrors( { + '{{slug}}': { + message: 'Please tick the box', + hidden: false, + }, + } ); + return; + } + clearValidationError( '{{slug}}' ); + }, [ + clearValidationError, + setValidationErrors, + checked, + setExtensionData, + ] ); + + const { validationError } = useSelect( ( select ) => { + const store = select( 'wc/store/validation' ); + return { + validationError: store.getValidationError( '{{slug}}' ), + }; + } ); + + return ( + <> + + { children || optInDefaultText } + + + { validationError?.hidden === false && ( +
+ + ⚠️ + + { validationError?.message } +
+ ) } + + ); +}; + +export default Block; diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache new file mode 100644 index 00000000000..ed70cbd7b67 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache @@ -0,0 +1,34 @@ +{ + "apiVersion": 2, + "name": "{{slug}}/checkout-newsletter-subscription", + "version": "2.0.0", + "title": "Newsletter Subscription!", + "category": "{{slug}}", + "description": "Adds a newsletter subscription checkbox to the checkout.", + "supports": { + "html": false, + "align": false, + "multiple": false, + "reusable": false + }, + "parent": [ + "woocommerce/checkout-contact-information-block" + ], + "attributes": { + "lock": { + "type": "object", + "default": { + "remove": true, + "move": true + } + }, + "text": { + "type": "string", + "source": "html", + "selector": ".wp-block-{{slug}}-checkout-newsletter-subscription", + "default": "" + } + }, + "textdomain": "{{slug}}", + "editorStyle": "file:../../../build/style-{{slug}}-checkout-newsletter-subscription-block.css" +} diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache new file mode 100644 index 00000000000..f55abeb2b72 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + RichText, + InspectorControls, +} from '@wordpress/block-editor'; +import { PanelBody } from '@wordpress/components'; +import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { getSetting } from '@woocommerce/settings'; +/** + * Internal dependencies + */ +import './style.scss'; +const { optInDefaultText } = getSetting( '{{slug}}_data', '' ); + +export const Edit = ( { attributes, setAttributes } ) => { + const { text } = attributes; + const blockProps = useBlockProps(); + return ( +
+ + + Options for the block go here. + + + + setAttributes( { text: value } ) } + /> +
+ ); +}; + +export const Save = ( { attributes } ) => { + const { text } = attributes; + return ( +
+ +
+ ); +}; \ No newline at end of file diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache new file mode 100644 index 00000000000..b93fa66ec9f --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache @@ -0,0 +1,14 @@ +/** + * External dependencies + */ +import { registerCheckoutBlock } from '@woocommerce/blocks-checkout'; +/** + * Internal dependencies + */ +import Block from './block'; +import metadata from './block.json'; + +registerCheckoutBlock( { + metadata, + component: Block, +} ); \ No newline at end of file diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache new file mode 100644 index 00000000000..7a6ea521ade --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { SVG } from '@wordpress/components'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { Edit, Save } from './edit'; +import metadata from './block.json'; +registerBlockType( metadata, { + icon: { + src: ( + + + + + + + ), + foreground: '#874FB9', + }, + edit: Edit, + save: Save, +} ); diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache new file mode 100644 index 00000000000..395ee85d530 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache @@ -0,0 +1,27 @@ +.wp-block-{{slug}}-checkout-newsletter-subscription { + margin: 20px 0; + padding-top: 4px; + padding-bottom: 4px; + display: flex; + align-items: flex-start; + + .block-editor-rich-text__editable { + vertical-align: middle; + line-height: 24px; + } + + .wc-block-components-checkbox { + margin-right: 16px; + margin-top: 0; + } +} + +.editor-styles-wrapper { + + .wp-block-{{slug}}-checkout-newsletter-subscription { + .wc-block-components-checkbox { + margin-right: 0; + margin-top: 0; + } + } +} diff --git a/packages/js/extend-cart-checkout-block/src/js/index.js.mustache b/packages/js/extend-cart-checkout-block/src/js/index.js.mustache index 238e1c1983a..a2ab7d7fa0d 100644 --- a/packages/js/extend-cart-checkout-block/src/js/index.js.mustache +++ b/packages/js/extend-cart-checkout-block/src/js/index.js.mustache @@ -9,14 +9,11 @@ import { getSetting } from '@woocommerce/settings'; */ import './style.scss'; -const exampleDataFromSettings = getSetting( '{{slug}}_data' ); - -/** - * Internal dependencies - */ import { registerFilters } from './filters'; import { ExampleComponent } from './ExampleComponent'; +const exampleDataFromSettings = getSetting( '{{slug}}_data' ); + const render = () => { return ( <> diff --git a/packages/js/extend-cart-checkout-block/webpack.config.js.mustache b/packages/js/extend-cart-checkout-block/webpack.config.js.mustache index c25a9a2f6f6..ce8447b6078 100644 --- a/packages/js/extend-cart-checkout-block/webpack.config.js.mustache +++ b/packages/js/extend-cart-checkout-block/webpack.config.js.mustache @@ -10,6 +10,23 @@ const defaultRules = defaultConfig.module.rules.filter( ( rule ) => { module.exports = { ...defaultConfig, + entry: { + index: path.resolve(process.cwd(), 'src', 'js', 'index.js'), + '{{slug}}-checkout-newsletter-subscription-block': path.resolve( + process.cwd(), + 'src', + 'js', + 'checkout-newsletter-subscription-block', + 'index.js' + ), + '{{slug}}-checkout-newsletter-subscription-block-frontend': path.resolve( + process.cwd(), + 'src', + 'js', + 'checkout-newsletter-subscription-block', + 'frontend.js' + ), + }, module: { ...defaultConfig.module, rules: [ From 04d6e8849c38c5a78a1cc98e31766feb8c632e8b Mon Sep 17 00:00:00 2001 From: louwie17 Date: Thu, 27 Oct 2022 12:52:29 -0300 Subject: [PATCH 085/149] Add name to select control popover slots (#35353) * Add name to SelectControlMenuSlot * Add changelogs * Update changelog * Fix typo in changelog --- .../components/changelog/fix-select-control-popover-slots | 4 ++++ .../js/components/src/experimental-select-control/menu.tsx | 5 ++++- .../fields/attribute-field/add-attribute-modal.tsx | 7 ++++++- .../products/fields/attribute-field/attribute-field.tsx | 4 +--- .../woocommerce/changelog/fix-select-control-popover-slots | 4 ++++ 5 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 packages/js/components/changelog/fix-select-control-popover-slots create mode 100644 plugins/woocommerce/changelog/fix-select-control-popover-slots diff --git a/packages/js/components/changelog/fix-select-control-popover-slots b/packages/js/components/changelog/fix-select-control-popover-slots new file mode 100644 index 00000000000..81d59f026ab --- /dev/null +++ b/packages/js/components/changelog/fix-select-control-popover-slots @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add name to exported popover slot used to display SelectControl Menu, so it is only used for SelectControl menus. diff --git a/packages/js/components/src/experimental-select-control/menu.tsx b/packages/js/components/src/experimental-select-control/menu.tsx index f4ad3a1878b..60a8cbb6e4e 100644 --- a/packages/js/components/src/experimental-select-control/menu.tsx +++ b/packages/js/components/src/experimental-select-control/menu.tsx @@ -52,6 +52,8 @@ export const Menu = ( { ) } > createPortal(
- + { /* @ts-expect-error name does exist on PopoverSlot see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L555 */ } +
, document.body ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx index 6e2e4e3764d..6ead6cd435e 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx @@ -5,7 +5,10 @@ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { trash } from '@wordpress/icons'; import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; -import { Form } from '@woocommerce/components'; +import { + Form, + __experimentalSelectControlMenuSlot as SelectControlMenuSlot, +} from '@woocommerce/components'; import { Button, Modal, @@ -310,6 +313,8 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { ); } } + { /* Add slot so select control menu renders correctly within Modal */ } + { showConfirmClose && ( = ( { ) } /> ) } -
@@ -179,7 +178,6 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { selectedAttributeIds={ value.map( ( attr ) => attr.id ) } /> ) } -
); }; diff --git a/plugins/woocommerce/changelog/fix-select-control-popover-slots b/plugins/woocommerce/changelog/fix-select-control-popover-slots new file mode 100644 index 00000000000..061d348413d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-select-control-popover-slots @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Remove Popover.Slot usage and make use of exported SelectControlMenuSlot. From 8d73a03f9712f2372a2fc3ec768d7efbd1408865 Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Thu, 27 Oct 2022 11:03:19 -0700 Subject: [PATCH 086/149] Add line number reference links to release posts for hooks (#35354) --- .../src/commands/analyzer/analyzer-lint.ts | 3 +- tools/code-analyzer/src/lib/hook-changes.ts | 49 +++++++++++++++---- tools/code-analyzer/src/lib/scan-changes.ts | 13 ++++- tools/code-analyzer/src/print.ts | 18 +++---- .../release-post/release-post-release.ts | 3 +- tools/release-posts/templates/hooks.ejs | 4 +- tools/release-posts/templates/templates.ejs | 4 +- 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts b/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts index 043d2ad3984..16da3295a0a 100644 --- a/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts +++ b/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts @@ -53,7 +53,8 @@ const program = new Command() sinceVersion, skipSchemaCheck, source, - base + base, + outputStyle ); if ( changes.templates.size ) { diff --git a/tools/code-analyzer/src/lib/hook-changes.ts b/tools/code-analyzer/src/lib/hook-changes.ts index 9ce4abb3f26..0bf4f3da5c3 100644 --- a/tools/code-analyzer/src/lib/hook-changes.ts +++ b/tools/code-analyzer/src/lib/hook-changes.ts @@ -2,6 +2,7 @@ * External dependencies */ import { getFilename, getPatches } from 'cli-core/src/util'; +import fs from 'node:fs'; /** * Internal dependencies @@ -20,9 +21,14 @@ export type HookChangeDescription = { hookType: string; changeType: 'new' | 'updated'; version: string; + ghLink: string; }; -export const scanForHookChanges = ( content: string, version: string ) => { +export const scanForHookChanges = ( + content: string, + version: string, + tmpRepoPath: string +) => { const changes: Map< string, HookChangeDescription > = new Map(); if ( ! content.match( /diff --git a\/(.+).php/g ) ) { @@ -73,14 +79,39 @@ export const scanForHookChanges = ( content: string, version: string ) => { const changeType = getHookChangeType( raw ); if ( ! hookName[ 2 ].startsWith( '-' ) ) { - changes.set( filePath, { - filePath, - name, - hookType, - description, - changeType, - version, - } ); + let ghLink = ''; + + fs.readFile( + tmpRepoPath + filePath, + 'utf-8', + function ( err, data ) { + if ( err ) { + console.error( err ); + } + + const reg = new RegExp( name ); + data.split( '\n' ).forEach( ( line, index ) => { + if ( line.match( reg ) ) { + const lineNum = index + 1; + + ghLink = `https://github.com/woocommerce/woocommerce/blob/${ version }/${ filePath.replace( + /(^\/)/, + '' + ) }#L${ lineNum }`; + } + } ); + + changes.set( filePath, { + filePath, + name, + hookType, + description, + changeType, + version, + ghLink, + } ); + } + ); } } } diff --git a/tools/code-analyzer/src/lib/scan-changes.ts b/tools/code-analyzer/src/lib/scan-changes.ts index 9e5dc0cd6a1..5042db58785 100644 --- a/tools/code-analyzer/src/lib/scan-changes.ts +++ b/tools/code-analyzer/src/lib/scan-changes.ts @@ -5,6 +5,7 @@ import { Logger } from 'cli-core/src/logger'; import { join } from 'path'; import { cloneRepo, generateDiff } from 'cli-core/src/git'; import { readFile } from 'fs/promises'; +import { execSync } from 'child_process'; /** * Internal dependencies @@ -20,7 +21,8 @@ export const scanForChanges = async ( sinceVersion: string, skipSchemaCheck: boolean, source: string, - base: string + base: string, + outputStyle: string ) => { Logger.startTask( `Making temporary clone of ${ source }...` ); const tmpRepoPath = await cloneRepo( source ); @@ -37,10 +39,17 @@ export const scanForChanges = async ( Logger.error ); + // Only checkout the compare version if we're in CLI mode. + if ( outputStyle === 'cli' ) { + execSync( `cd ${ tmpRepoPath } && git checkout ${ compareVersion }`, { + stdio: 'pipe', + } ); + } + const pluginPath = join( tmpRepoPath, 'plugins/woocommerce' ); Logger.startTask( 'Detecting hook changes...' ); - const hookChanges = scanForHookChanges( diff, sinceVersion ); + const hookChanges = scanForHookChanges( diff, sinceVersion, tmpRepoPath ); Logger.endTask(); Logger.startTask( 'Detecting template changes...' ); diff --git a/tools/code-analyzer/src/print.ts b/tools/code-analyzer/src/print.ts index a522df7676e..1c36f69578a 100644 --- a/tools/code-analyzer/src/print.ts +++ b/tools/code-analyzer/src/print.ts @@ -19,11 +19,10 @@ export const printTemplateResults = ( title: string, log: ( s: string ) => void ): void => { - //[code,title,message] if ( output === 'github' ) { let opt = '\\n\\n### Template changes:'; for ( const { filePath, code, message } of data ) { - opt += `\\n* **file:** ${ filePath }`; + opt += `\\n* **File:** ${ filePath }`; opt += `\\n * ${ code.toUpperCase() }: ${ message }`; log( `::${ code } file=${ filePath },line=1,title=${ title }::${ message }` @@ -56,12 +55,6 @@ export const printHookResults = ( sectionTitle: string, log: ( s: string ) => void ) => { - // [ - // 'NOTICE', - // title, - // message, - // description, - // ] if ( output === 'github' ) { let opt = '\\n\\n### New hooks:'; for ( const { @@ -70,9 +63,9 @@ export const printHookResults = ( version, description, hookType, - changeType, + changeType } of data ) { - opt += `\\n* **file:** ${ filePath }`; + opt += `\\n* **File:** ${ filePath }`; const cliMessage = `**${ name }** introduced in ${ version }`; const ghMessage = `\\'${ name }\\' introduced in ${ version }`; @@ -96,6 +89,7 @@ export const printHookResults = ( description, hookType, changeType, + ghLink, } of data ) { const cliMessage = `**${ name }** introduced in ${ version }`; const ghMessage = `\\'${ name }\\' introduced in ${ version }`; @@ -106,8 +100,10 @@ export const printHookResults = ( log( '---------------------------------------------------' ); log( `HOOK: ${ name }: ${ description }` ); log( '---------------------------------------------------' ); - log( `NOTICE | ${ title } | ${ message }` ); + log( `GITHUB: ${ ghLink }` ); log( '---------------------------------------------------' ); + log( `NOTICE | ${ title } | ${ message }` ); + log( '---------------------------------------------------\n' ); } } }; diff --git a/tools/release-posts/commands/release-post/release-post-release.ts b/tools/release-posts/commands/release-post/release-post-release.ts index 6a765cf36d1..60308f82ffd 100644 --- a/tools/release-posts/commands/release-post/release-post-release.ts +++ b/tools/release-posts/commands/release-post/release-post-release.ts @@ -74,7 +74,8 @@ const program = new Command() currentVersion, false, 'https://github.com/woocommerce/woocommerce.git', - previousVersion.toString() + previousVersion.toString(), + 'cli' ); const schemaChanges = changes.schema.filter( diff --git a/tools/release-posts/templates/hooks.ejs b/tools/release-posts/templates/hooks.ejs index 898e394c197..6133b5cae76 100644 --- a/tools/release-posts/templates/hooks.ejs +++ b/tools/release-posts/templates/hooks.ejs @@ -10,11 +10,13 @@ Filter Description + GitHub Link - <% changes.hooks.forEach(({ name, description }) => { %> + <% changes.hooks.forEach(({ name, description, ghLink }) => { %> <%= name %> <%= description %> + Link <% }) %> diff --git a/tools/release-posts/templates/templates.ejs b/tools/release-posts/templates/templates.ejs index 0ecc08b816c..ec7d93ecf95 100644 --- a/tools/release-posts/templates/templates.ejs +++ b/tools/release-posts/templates/templates.ejs @@ -12,11 +12,11 @@ - + <% changes.templates.forEach((change) => { %> - <%= change %> + <%= change.filePath %> <% }) %> From ec9ef7458e17d35f48523854f92dbcd79cab29b1 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Fri, 28 Oct 2022 00:06:50 +0530 Subject: [PATCH 087/149] Remove address indexes from list of internal keys. (#35192) * Override filter_meta_data method, since it should be a no-op anyway. * Add changelog. * Not include address indexes from filtered data. * Applied coding standards. --- .../woocommerce/changelog/fix-duplicate_meta | 4 +++ .../Orders/OrdersTableDataStore.php | 29 +++++++++++++++++++ .../Orders/OrdersTableDataStoreTests.php | 28 ++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-duplicate_meta diff --git a/plugins/woocommerce/changelog/fix-duplicate_meta b/plugins/woocommerce/changelog/fix-duplicate_meta new file mode 100644 index 00000000000..7eb2163f78e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-duplicate_meta @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Override filter_meta_data method, since it should be a no-op anyway. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 92229dfd2c0..826cc0ce5c6 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1031,6 +1031,35 @@ WHERE $order->set_object_read( true ); } + /** + * For post based data stores, this was used to filter internal meta data. For custom tables, technically there is no internal meta data, + * (i.e. we store all core data as properties for the order, and not in meta data). So this method is a no-op. + * + * Except that some meta such as billing_address_index and shipping_address_index are infact stored in meta data, so we need to filter those out. + * + * However, declaring $internal_meta_keys is still required so that our backfill and other comparison checks works as expected. + * + * @param \WC_Data $object Object to filter meta data for. + * @param array $raw_meta_data Raw meta data. + * + * @return array Filtered meta data. + */ + public function filter_raw_meta_data( &$object, $raw_meta_data ) { + $filtered_meta_data = parent::filter_raw_meta_data( $object, $raw_meta_data ); + $allowed_keys = array( + '_billing_address_index', + '_shipping_address_index', + ); + $allowed_meta = array_filter( + $raw_meta_data, + function( $meta ) use ( $allowed_keys ) { + return in_array( $meta->meta_key, $allowed_keys, true ); + } + ); + + return array_merge( $allowed_meta, $filtered_meta_data ); + } + /** * Sync order to/from posts tables if we are able to detect difference between order and posts but the sync is enabled. * diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 4c838c25eab..37f4f850469 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -1867,4 +1867,32 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $product->save(); $this->assertFalse( wc_get_order( $product->get_id() ) ); } + + /** + * @testDox Test that we are not duplicating address indexing when updating. + */ + public function test_address_index_saved_on_update() { + global $wpdb; + $this->toggle_cot( true ); + $this->disable_cot_sync(); + $order = new WC_Order(); + $order->set_billing_address_1( '123 Main St' ); + $order->save(); + + $this->assertTrue( false !== strpos( $order->get_meta( '_billing_address_index', true ), '123 Main St' ) ); + $order = wc_get_order( $order->get_id() ); + $order->set_billing_address_2( 'Apt 1' ); + $order->save(); + + $order_meta_table = $this->sut::get_meta_table_name(); + // Assert that we are not duplicating address indexes. + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$order_meta_table} WHERE order_id = %d AND meta_key = '_billing_address_index'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $order->get_id() + ) + ); + + $this->assertEquals( 1, $result ); + } } From a18f1bde92b5e17fbefe8a433035f0195ab17361 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 28 Oct 2022 11:29:03 +0800 Subject: [PATCH 088/149] Migrate @woocommerce/data report store to TS (#35048) * Migrate reports store to TS * Add changelog * Update reports types --- .../data/changelog/dev-migrate-reports-to-ts | 4 + .../{action-types.js => action-types.ts} | 2 +- packages/js/data/src/reports/actions.js | 45 -- packages/js/data/src/reports/actions.ts | 76 +++ packages/js/data/src/reports/constants.ts | 2 +- packages/js/data/src/reports/index.js | 25 - packages/js/data/src/reports/index.ts | 60 ++ packages/js/data/src/reports/reducer.js | 47 -- packages/js/data/src/reports/reducer.ts | 62 ++ packages/js/data/src/reports/resolvers.js | 75 --- packages/js/data/src/reports/resolvers.ts | 110 ++++ packages/js/data/src/reports/selectors.js | 26 - packages/js/data/src/reports/selectors.ts | 57 ++ .../reports/test/{reducer.js => reducer.ts} | 19 +- packages/js/data/src/reports/types.ts | 553 ++++++++++++++++++ .../data/src/reports/{utils.js => utils.ts} | 211 +++++-- 16 files changed, 1082 insertions(+), 292 deletions(-) create mode 100644 packages/js/data/changelog/dev-migrate-reports-to-ts rename packages/js/data/src/reports/{action-types.js => action-types.ts} (93%) delete mode 100644 packages/js/data/src/reports/actions.js create mode 100644 packages/js/data/src/reports/actions.ts delete mode 100644 packages/js/data/src/reports/index.js create mode 100644 packages/js/data/src/reports/index.ts delete mode 100644 packages/js/data/src/reports/reducer.js create mode 100644 packages/js/data/src/reports/reducer.ts delete mode 100644 packages/js/data/src/reports/resolvers.js create mode 100644 packages/js/data/src/reports/resolvers.ts delete mode 100644 packages/js/data/src/reports/selectors.js create mode 100644 packages/js/data/src/reports/selectors.ts rename packages/js/data/src/reports/test/{reducer.js => reducer.ts} (80%) create mode 100644 packages/js/data/src/reports/types.ts rename packages/js/data/src/reports/{utils.js => utils.ts} (79%) diff --git a/packages/js/data/changelog/dev-migrate-reports-to-ts b/packages/js/data/changelog/dev-migrate-reports-to-ts new file mode 100644 index 00000000000..c3d939a24f0 --- /dev/null +++ b/packages/js/data/changelog/dev-migrate-reports-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate reports to TS diff --git a/packages/js/data/src/reports/action-types.js b/packages/js/data/src/reports/action-types.ts similarity index 93% rename from packages/js/data/src/reports/action-types.js rename to packages/js/data/src/reports/action-types.ts index 5ca88da5207..082280de8a0 100644 --- a/packages/js/data/src/reports/action-types.js +++ b/packages/js/data/src/reports/action-types.ts @@ -3,6 +3,6 @@ const TYPES = { SET_STAT_ERROR: 'SET_STAT_ERROR', SET_REPORT_ITEMS: 'SET_REPORT_ITEMS', SET_REPORT_STATS: 'SET_REPORT_STATS', -}; +} as const; export default TYPES; diff --git a/packages/js/data/src/reports/actions.js b/packages/js/data/src/reports/actions.js deleted file mode 100644 index b6449e659f0..00000000000 --- a/packages/js/data/src/reports/actions.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Internal dependencies - */ -import { getResourceName } from '../utils'; -import TYPES from './action-types'; - -export function setReportItemsError( endpoint, query, error ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_ITEM_ERROR, - resourceName, - error, - }; -} - -export function setReportItems( endpoint, query, items ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_REPORT_ITEMS, - resourceName, - items, - }; -} - -export function setReportStats( endpoint, query, stats ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_REPORT_STATS, - resourceName, - stats, - }; -} - -export function setReportStatsError( endpoint, query, error ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_STAT_ERROR, - resourceName, - error, - }; -} diff --git a/packages/js/data/src/reports/actions.ts b/packages/js/data/src/reports/actions.ts new file mode 100644 index 00000000000..ffebf70e60c --- /dev/null +++ b/packages/js/data/src/reports/actions.ts @@ -0,0 +1,76 @@ +/** + * Internal dependencies + */ +import { getResourceName } from '../utils'; +import TYPES from './action-types'; +import { + ReportItemsEndpoint, + ReportStatEndpoint, + ReportQueryParams, + ReportStatQueryParams, + ReportItemObject, + ReportStatObject, +} from './types'; + +export function setReportItemsError( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams, + error: unknown +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_ITEM_ERROR, + resourceName, + error, + }; +} + +export function setReportItems( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams, + items: ReportItemObject +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_REPORT_ITEMS, + resourceName, + items, + }; +} + +export function setReportStats( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams, + stats: ReportStatObject +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_REPORT_STATS, + resourceName, + stats, + }; +} + +export function setReportStatsError( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams, + error: unknown +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_STAT_ERROR, + resourceName, + error, + }; +} + +export type Action = ReturnType< + | typeof setReportItems + | typeof setReportItemsError + | typeof setReportStats + | typeof setReportStatsError +>; diff --git a/packages/js/data/src/reports/constants.ts b/packages/js/data/src/reports/constants.ts index fdf8db22cf4..b0249f3966c 100644 --- a/packages/js/data/src/reports/constants.ts +++ b/packages/js/data/src/reports/constants.ts @@ -1,4 +1,4 @@ /** * Internal dependencies */ -export const STORE_NAME = 'wc/admin/reports'; +export const STORE_NAME = 'wc/admin/reports' as const; diff --git a/packages/js/data/src/reports/index.js b/packages/js/data/src/reports/index.js deleted file mode 100644 index 0172e6a3cdc..00000000000 --- a/packages/js/data/src/reports/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * External dependencies - */ - -import { registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './constants'; -import * as selectors from './selectors'; -import * as actions from './actions'; -import * as resolvers from './resolvers'; -import controls from '../controls'; -import reducer from './reducer'; - -registerStore( STORE_NAME, { - reducer, - actions, - controls, - selectors, - resolvers, -} ); - -export const REPORTS_STORE_NAME = STORE_NAME; diff --git a/packages/js/data/src/reports/index.ts b/packages/js/data/src/reports/index.ts new file mode 100644 index 00000000000..7d738c15c78 --- /dev/null +++ b/packages/js/data/src/reports/index.ts @@ -0,0 +1,60 @@ +/** + * External dependencies + */ + +import { registerStore } from '@wordpress/data'; +import { Reducer, AnyAction } from 'redux'; +import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from './constants'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; +import controls from '../controls'; +import reducer, { State } from './reducer'; +import { WPDataActions, WPDataSelectors } from '../types'; +import { + ReportItemObjectInfer, + ReportItemsEndpoint, + ReportQueryParams, + ReportStatEndpoint, + ReportStatObjectInfer, + ReportStatQueryParams, +} from './types'; +export * from './types'; +export type { State }; + +registerStore( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + controls, + selectors, + resolvers, +} ); + +export const REPORTS_STORE_NAME = STORE_NAME; + +export type ReportsSelect = WPDataSelectors & + Omit< + SelectFromMap< typeof selectors >, + 'getReportItems' | 'getReportStats' + > & { + getReportItems: < T >( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams + ) => ReportItemObjectInfer< T >; + getReportStats: < T >( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams + ) => ReportStatObjectInfer< T >; + }; + +declare module '@wordpress/data' { + function dispatch( + key: typeof STORE_NAME + ): DispatchFromMap< typeof actions & WPDataActions >; + function select( key: typeof STORE_NAME ): ReportsSelect; +} diff --git a/packages/js/data/src/reports/reducer.js b/packages/js/data/src/reports/reducer.js deleted file mode 100644 index 562aea0dc23..00000000000 --- a/packages/js/data/src/reports/reducer.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Internal dependencies - */ -import TYPES from './action-types'; - -const reports = ( - state = { - itemErrors: {}, - items: {}, - statErrors: {}, - stats: {}, - }, - { type, items, stats, error, resourceName } -) => { - switch ( type ) { - case TYPES.SET_REPORT_ITEMS: - return { - ...state, - items: { ...state.items, [ resourceName ]: items }, - }; - case TYPES.SET_REPORT_STATS: - return { - ...state, - stats: { ...state.stats, [ resourceName ]: stats }, - }; - case TYPES.SET_ITEM_ERROR: - return { - ...state, - itemErrors: { - ...state.itemErrors, - [ resourceName ]: error, - }, - }; - case TYPES.SET_STAT_ERROR: - return { - ...state, - statErrors: { - ...state.statErrors, - [ resourceName ]: error, - }, - }; - default: - return state; - } -}; - -export default reports; diff --git a/packages/js/data/src/reports/reducer.ts b/packages/js/data/src/reports/reducer.ts new file mode 100644 index 00000000000..1614c794d50 --- /dev/null +++ b/packages/js/data/src/reports/reducer.ts @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { Action } from './actions'; +import { ReportState } from './types'; + +const initialState: ReportState = { + itemErrors: {}, + items: {}, + statErrors: {}, + stats: {}, +}; +const reducer: Reducer< ReportState, Action > = ( + state = initialState, + action +) => { + switch ( action.type ) { + case TYPES.SET_REPORT_ITEMS: + return { + ...state, + items: { + ...state.items, + [ action.resourceName ]: action.items, + }, + }; + case TYPES.SET_REPORT_STATS: + return { + ...state, + stats: { + ...state.stats, + [ action.resourceName ]: action.stats, + }, + }; + case TYPES.SET_ITEM_ERROR: + return { + ...state, + itemErrors: { + ...state.itemErrors, + [ action.resourceName ]: action.error, + }, + }; + case TYPES.SET_STAT_ERROR: + return { + ...state, + statErrors: { + ...state.statErrors, + [ action.resourceName ]: action.error, + }, + }; + default: + return state; + } +}; + +export type State = ReturnType< typeof reducer >; +export default reducer; diff --git a/packages/js/data/src/reports/resolvers.js b/packages/js/data/src/reports/resolvers.js deleted file mode 100644 index 80224a3eaed..00000000000 --- a/packages/js/data/src/reports/resolvers.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { addQueryArgs } from '@wordpress/url'; - -/** - * Internal dependencies - */ -import { fetchWithHeaders } from '../controls'; -import { NAMESPACE } from '../constants'; -import { - setReportItemsError, - setReportStatsError, - setReportItems, - setReportStats, -} from './actions'; - -export function* getReportItems( endpoint, query ) { - const fetchArgs = { - parse: false, - path: addQueryArgs( `${ NAMESPACE }/reports/${ endpoint }`, query ), - }; - - try { - const response = yield fetchWithHeaders( fetchArgs ); - const data = response.data; - const totalResults = parseInt( - response.headers.get( 'x-wp-total' ), - 10 - ); - const totalPages = parseInt( - response.headers.get( 'x-wp-totalpages' ), - 10 - ); - - yield setReportItems( endpoint, query, { - data, - totalResults, - totalPages, - } ); - } catch ( error ) { - yield setReportItemsError( endpoint, query, error ); - } -} - -export function* getReportStats( endpoint, query ) { - const fetchArgs = { - parse: false, - path: addQueryArgs( - `${ NAMESPACE }/reports/${ endpoint }/stats`, - query - ), - }; - - try { - const response = yield fetchWithHeaders( fetchArgs ); - const data = response.data; - const totalResults = parseInt( - response.headers.get( 'x-wp-total' ), - 10 - ); - const totalPages = parseInt( - response.headers.get( 'x-wp-totalpages' ), - 10 - ); - - yield setReportStats( endpoint, query, { - data, - totalResults, - totalPages, - } ); - } catch ( error ) { - yield setReportStatsError( endpoint, query, error ); - } -} diff --git a/packages/js/data/src/reports/resolvers.ts b/packages/js/data/src/reports/resolvers.ts new file mode 100644 index 00000000000..249fe9ddc83 --- /dev/null +++ b/packages/js/data/src/reports/resolvers.ts @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { fetchWithHeaders } from '../controls'; +import { NAMESPACE } from '../constants'; +import { + setReportItemsError, + setReportStatsError, + setReportItems, + setReportStats, +} from './actions'; +import { + ReportItemsEndpoint, + ReportStatEndpoint, + ReportQueryParams, + ReportStatQueryParams, + ReportItemObject, + ReportStatObject, +} from './types'; + +const getIntHeaderValues = ( + endpoint: string, + response: { + headers: Map< string, string >; + data: unknown; + }, + keys: string[] +) => { + return keys.map( ( key ) => { + const value = response.headers.get( key ); + if ( value === undefined ) { + throw new Error( + `Malformed response from server. '${ key }' header is missing when retriving ./report/${ endpoint }.` + ); + } + return parseInt( value, 10 ); + } ); +}; + +export function* getReportItems( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams +) { + const fetchArgs = { + parse: false, + path: addQueryArgs( `${ NAMESPACE }/reports/${ endpoint }`, query ), + }; + + try { + const response: { + headers: Map< string, string >; + data: ReportItemObject[ 'data' ]; + } = yield fetchWithHeaders( fetchArgs ); + const data = response.data; + + const [ totalResults, totalPages ] = getIntHeaderValues( + endpoint, + response, + [ 'x-wp-total', 'x-wp-totalpages' ] + ); + + yield setReportItems( endpoint, query, { + data, + totalResults, + totalPages, + } ); + } catch ( error ) { + yield setReportItemsError( endpoint, query, error ); + } +} + +export function* getReportStats( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams +) { + const fetchArgs = { + parse: false, + path: addQueryArgs( + `${ NAMESPACE }/reports/${ endpoint }/stats`, + query + ), + }; + + try { + const response: { + headers: Map< string, string >; + data: ReportStatObject[ 'data' ]; + } = yield fetchWithHeaders( fetchArgs ); + const data = response.data; + + const [ totalResults, totalPages ] = getIntHeaderValues( + endpoint, + response, + [ 'x-wp-total', 'x-wp-totalpages' ] + ); + + yield setReportStats( endpoint, query, { + data, + totalResults, + totalPages, + } ); + } catch ( error ) { + yield setReportStatsError( endpoint, query, error ); + } +} diff --git a/packages/js/data/src/reports/selectors.js b/packages/js/data/src/reports/selectors.js deleted file mode 100644 index 6ac94167cc1..00000000000 --- a/packages/js/data/src/reports/selectors.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Internal dependencies - */ -import { getResourceName } from '../utils'; - -const EMPTY_OBJECT = {}; - -export const getReportItemsError = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.itemErrors[ resourceName ] || false; -}; - -export const getReportItems = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.items[ resourceName ] || EMPTY_OBJECT; -}; - -export const getReportStats = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.stats[ resourceName ] || EMPTY_OBJECT; -}; - -export const getReportStatsError = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.statErrors[ resourceName ] || false; -}; diff --git a/packages/js/data/src/reports/selectors.ts b/packages/js/data/src/reports/selectors.ts new file mode 100644 index 00000000000..94f44df437d --- /dev/null +++ b/packages/js/data/src/reports/selectors.ts @@ -0,0 +1,57 @@ +/** + * Internal dependencies + */ +import { getResourceName } from '../utils'; +import { + ReportState, + ReportItemsEndpoint, + ReportStatEndpoint, + ReportQueryParams, + ReportStatQueryParams, + ReportItemObjectInfer, + ReportStatObjectInfer, +} from './types'; + +const EMPTY_OBJECT = {} as const; + +export const getReportItemsError = ( + state: ReportState, + endpoint: ReportItemsEndpoint, + query: ReportQueryParams +) => { + const resourceName = getResourceName( endpoint, query ); + return state.itemErrors[ resourceName ] || false; +}; + +export const getReportItems = < T >( + state: ReportState, + endpoint: ReportItemsEndpoint, + query: ReportQueryParams +): ReportItemObjectInfer< T > => { + const resourceName = getResourceName( endpoint, query ); + return ( + ( state.items[ resourceName ] as ReportItemObjectInfer< T > ) || + EMPTY_OBJECT + ); +}; + +export const getReportStats = < T >( + state: ReportState, + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams +): ReportStatObjectInfer< T > => { + const resourceName = getResourceName( endpoint, query ); + return ( + ( state.stats[ resourceName ] as ReportStatObjectInfer< T > ) || + EMPTY_OBJECT + ); +}; + +export const getReportStatsError = ( + state: ReportState, + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams +) => { + const resourceName = getResourceName( endpoint, query ); + return state.statErrors[ resourceName ] || false; +}; diff --git a/packages/js/data/src/reports/test/reducer.js b/packages/js/data/src/reports/test/reducer.ts similarity index 80% rename from packages/js/data/src/reports/test/reducer.js rename to packages/js/data/src/reports/test/reducer.ts index 9303f407840..83c78d602a9 100644 --- a/packages/js/data/src/reports/test/reducer.js +++ b/packages/js/data/src/reports/test/reducer.ts @@ -7,6 +7,7 @@ */ import reducer from '../reducer'; import TYPES from '../action-types'; +import { Action } from '../actions'; const defaultState = { itemErrors: {}, @@ -17,7 +18,7 @@ const defaultState = { describe( 'reports reducer', () => { it( 'should return a default state', () => { - const state = reducer( undefined, {} ); + const state = reducer( undefined, {} as Action ); expect( state ).toEqual( defaultState ); expect( state ).not.toBe( defaultState ); } ); @@ -26,6 +27,7 @@ describe( 'reports reducer', () => { const state = reducer( defaultState, { type: TYPES.SET_REPORT_ITEMS, resourceName: 'test-resource-items', + // @ts-expect-error This is a test. items: [ 1, 2 ], } ); @@ -38,6 +40,7 @@ describe( 'reports reducer', () => { const state = reducer( defaultState, { type: TYPES.SET_REPORT_STATS, resourceName: 'test-resource-stats', + // @ts-expect-error This is a test. stats: [ 3, 4 ], } ); @@ -53,9 +56,10 @@ describe( 'reports reducer', () => { error: { code: 'error' }, } ); - expect( state.itemErrors[ 'test-resource-items' ].code ).toBe( - 'error' - ); + expect( + ( state.itemErrors[ 'test-resource-items' ] as { code: string } ) + .code + ).toBe( 'error' ); } ); it( 'should handle SET_STAT_ERROR', () => { @@ -65,8 +69,9 @@ describe( 'reports reducer', () => { error: { code: 'error' }, } ); - expect( state.statErrors[ 'test-resource-stats' ].code ).toBe( - 'error' - ); + expect( + ( state.statErrors[ 'test-resource-stats' ] as { code: string } ) + .code + ).toBe( 'error' ); } ); } ); diff --git a/packages/js/data/src/reports/types.ts b/packages/js/data/src/reports/types.ts new file mode 100644 index 00000000000..e1859d58c79 --- /dev/null +++ b/packages/js/data/src/reports/types.ts @@ -0,0 +1,553 @@ +/** + * Internal dependencies + */ +import { getReportTableQuery, getRequestQuery } from './utils'; + +export type ReportItemsEndpoint = + | 'customers' + | 'products' + | 'varitations' + | 'orders' + | 'categories' + | 'taxes' + | 'coupons' + | 'stock' + | 'downloads' + | 'performance_indicator'; + +export type ReportStatEndpoint = + | 'products' + | 'variations' + | 'revenue' + | 'orders' + | 'taxes' + | 'coupons' + | 'customers'; + +export type ReportQueryParams = ReturnType< typeof getReportTableQuery >; +export type ReportStatQueryParams = ReturnType< typeof getRequestQuery >; + +export type CustomerReport = { + /** Customer ID. */ + id: number; + /** User ID. */ + user_id: number; + /** Name. */ + name: string; + /** Username. */ + username: string; + /** Country / Region. */ + country: string; + /** City. */ + city: string; + /** Region. */ + state: string; + /** Postal code. */ + postcode: string; + /** Date registered. */ + date_registered: string | null; + /** Date registered GMT. */ + date_registered_gmt: string | null; + /** Date last active. */ + date_last_active: string | null; + /** Date last active GMT. */ + date_last_active_gmt: string | null; + /** Order count. */ + orders_count: number; + /** Total spend. */ + total_spend: number; + /** Avg order value. */ + avg_order_value: number; +}; + +export type ProductReport = { + /** Product ID. */ + product_id: number; + /** Number of items sold. */ + items_sold: number; + /** Total Net sales of all items sold. */ + net_revenue: number; + /** Number of orders product appeared in. */ + orders_count: number; + extended_info: { + /** Product name. */ + name: string; + /** Product price. */ + price: number; + /** Product image. */ + image: string; + /** Product link. */ + permalink: string; + /** Product category IDs. */ + category_ids: Array< number >; + /** Product inventory status. */ + stock_status: string; + /** Product inventory quantity. */ + stock_quantity: number; + /** Product inventory threshold for low stock. */ + low_stock_amount: number; + /** Product variations IDs. */ + variations: Array< number >; + /** Product SKU. */ + sku: string; + }; +}; + +export type VariationReport = { + /** Product ID. */ + product_id: number; + /** Product ID. */ + variation_id: number; + /** Number of items sold. */ + items_sold: number; + /** Total Net sales of all items sold. */ + net_revenue: number; + /** Number of orders product appeared in. */ + orders_count: number; + extended_info: { + /** Product name. */ + name: string; + /** Product price. */ + price: number; + /** Product image. */ + image: string; + /** Product link. */ + permalink: string; + /** Product attributes. */ + attributes: Array< { + id: number; + name: string; + position: number; + visible: boolean; + variation: boolean; + options: string[]; + } >; + /** Product inventory status. */ + stock_status: string; + /** Product inventory quantity. */ + stock_quantity: number; + /** Product inventory threshold for low stock. */ + low_stock_amount: number; + }; +}; + +export type OrderReport = { + /** Order ID. */ + order_id: number; + /** Order Number. */ + order_number: string; + /** Date the order was created, in the site's timezone. */ + date_created: string | null; + /** Date the order was created, as GMT. */ + date_created_gmt: string | null; + /** Order status. */ + status: string; + /** Customer ID. */ + customer_id: number; + /** Number of items sold. */ + num_items_sold: number; + /** Net total revenue. */ + net_total: number; + /** Net total revenue (formatted). */ + total_formatted: string; + /** Returning or new customer. */ + customer_type: string; + extended_info: { + /** List of order product IDs, names, quantities. */ + products: Array< { + id: string; + name: string; + quantity: string; + } >; + /** List of order coupons. */ + coupons: Array< { + id: string; + code: string; + } >; + /** Order customer information. */ + customer: { + customer_id: number; + user_id: string; + username: string; + first_name: string; + last_name: string; + email: string; + date_last_active: string; + date_registered: string; + country: string; + postcode: string; + city: string; + state: string; + }; + }; +}; + +export type CategoriesReport = { + /** Category ID. */ + category_id: number; + /** Amount of items sold. */ + items_sold: number; + /** Total sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; + /** Amount of products. */ + products_count: number; + extended_info: { + /** Category name. */ + name: string; + }; +}; + +export type TaxesReport = { + /** Tax rate ID. */ + tax_rate_id: number; + /** Tax rate name. */ + name: string; + /** Tax rate. */ + tax_rate: number; + /** Country / Region. */ + country: string; + /** State. */ + state: string; + /** Priority. */ + priority: number; + /** Total tax. */ + total_tax: number; + /** Order tax. */ + order_tax: number; + /** Shipping tax. */ + shipping_tax: number; + /** Number of orders. */ + orders_count: number; +}; + +export type CouponReport = { + /** Coupon ID. */ + coupon_id: number; + /** Net discount amount. */ + amount: number; + /** Number of orders. */ + orders_count: number; + /** undefined */ + extended_info: { + /** Coupon code. */ + code: string; + /** Coupon creation date. */ + date_created: string | null; + /** Coupon creation date in GMT. */ + date_created_gmt: string | null; + /** Coupon expiration date. */ + date_expires: string | null; + /** Coupon expiration date in GMT. */ + date_expires_gmt: string | null; + /** Coupon discount type. */ + discount_type: 'percent' | 'fixed_cart' | 'fixed_product'; + }; +}; + +export type StockReport = { + /** Unique identifier for the resource. */ + id: number; + /** Product parent ID. */ + parent_id: number; + /** Product name. */ + name: string; + /** Unique identifier. */ + sku: string; + /** Stock status. */ + stock_status: 'instock' | 'outofstock' | 'onbackorder'; + /** Stock quantity. */ + stock_quantity: number; + /** Manage stock. */ + manage_stock: boolean; +}; + +export type DownloadReport = { + /** ID. */ + id: number; + /** Product ID. */ + product_id: number; + /** The date of the download, in the site's timezone. */ + date: string | null; + /** The date of the download, as GMT. */ + date_gmt: string | null; + /** Download ID. */ + download_id: string; + /** File name. */ + file_name: string; + /** File URL. */ + file_path: string; + /** Order ID. */ + order_id: number; + /** Order Number. */ + order_number: string; + /** User ID for the downloader. */ + user_id: number; + /** User name of the downloader. */ + username: string; + /** IP address for the downloader. */ + ip_address: string; +}; + +export type PerformanceIndicatorReport = { + /** Unique identifier for the resource. */ + stat: + | 'revenue/total_sales' + | 'revenue/net_revenue' + | 'revenue/shipping' + | 'revenue/refunds' + | 'revenue/gross_sales' + | 'orders/orders_count' + | 'orders/avg_order_value' + | 'products/items_sold' + | 'variations/items_sold' + | 'coupons/amount' + | 'coupons/orders_count' + | 'taxes/total_tax' + | 'taxes/order_tax' + | 'taxes/shipping_tax' + | 'downloads/download_count'; + /** The specific chart this stat referrers to. */ + chart: string; + /** Human readable label for the stat. */ + label: string; +}; + +export type ReportItemObject = { + data: + | CustomerReport + | ProductReport + | VariationReport + | CouponReport + | TaxesReport + | StockReport + | DownloadReport + | OrderReport + | CategoriesReport + | PerformanceIndicatorReport; + totalResults: number; + totalPages: number; +}; + +export type ReportItemObjectInfer< T > = { + data: T extends 'customers' + ? CustomerReport + : T extends 'products' + ? ProductReport + : T extends 'variations' + ? VariationReport + : T extends 'orders' + ? OrderReport + : T extends 'categories' + ? CategoriesReport + : T extends 'taxes' + ? TaxesReport + : T extends 'coupons' + ? CouponReport + : T extends 'stock' + ? StockReport + : T extends 'downloads' + ? DownloadReport + : T extends 'performance_indicators' + ? PerformanceIndicatorReport + : never; + totalResults: number; + totalPages: number; +}; + +type SubTotals = { + /** Number of product items sold. */ + items_sold: number; + /** Net sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; +}; + +type Interval = { + /** Type of interval. */ + interval: 'day' | 'week' | 'month' | 'year'; + /** The date the report start, in the site's timezone. */ + date_start: string | null; + /** The date the report start, as GMT. */ + date_start_gmt: string | null; + /** The date the report end, in the site's timezone. */ + date_end: string | null; + /** The date the report end, as GMT. */ + date_end_gmt: string | null; + /** Interval subtotals. */ + subtotals: SubTotals & { + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; +}; + +export type Segment = { + /** Segment identificator. */ + segment_id: number; + /** Human readable segment label, either product or variation name. */ + segment_label: 'day' | 'week' | 'month' | 'year'; + /** Interval subtotals. */ + subtotals: SubTotals; +}; + +export type ProductReportStat = { + totals: { + /** Number of product items sold. */ + items_sold: number; + /** Net sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type VariationsReportStat = { + totals: { + /** Number of variation items sold. */ + items_sold: number; + /** Net sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type RevenueReportStat = { + totals: { + /** Total sales. */ + total_sales: number; + /** Net sales. */ + net_revenue: number; + /** Amount discounted by coupons. */ + coupons: number; + /** Unique coupons count. */ + coupons_count: number; + /** Total of shipping. */ + shipping: number; + /** Total of taxes. */ + taxes: number; + /** Total of returns. */ + refunds: number; + /** Number of orders. */ + orders_count: number; + /** Items sold. */ + num_items_sold: number; + /** Products sold. */ + products: number; + /** Gross sales. */ + gross_sales: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type OrderReportStat = { + totals: { + /** Number of downloads. */ + download_count: number; + }; + intervals: Array< Interval >; +}; + +export type TaxesReportStat = { + totals: { + /** Total tax. */ + total_tax: number; + /** Order tax. */ + order_tax: number; + /** Shipping tax. */ + shipping_tax: number; + /** Number of orders. */ + orders_count: number; + /** Amount of tax codes. */ + tax_codes: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type CouponsReportStat = { + totals: { + /** Net discount amount. */ + amount: number; + /** Number of coupons. */ + coupons_count: number; + /** Number of discounted orders. */ + orders_count: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type CustomersReportStat = { + totals: { + /** Number of customers. */ + customers_count: number; + /** Average number of orders. */ + avg_orders_count: number; + /** Average total spend per customer. */ + avg_total_spend: number; + /** Average AOV per customer. */ + avg_avg_order_value: number; + }; + intervals: Array< Interval >; +}; + +export type ReportStatObject = { + data: + | ProductReportStat + | VariationsReportStat + | RevenueReportStat + | OrderReportStat + | TaxesReportStat + | CouponsReportStat + | CustomersReportStat; + totalResults: number; + totalPages: number; +}; + +export type ReportStatObjectInfer< T > = { + data: T extends 'products' + ? ProductReportStat + : T extends 'variations' + ? VariationsReportStat + : T extends 'revenue' + ? RevenueReportStat + : T extends 'orders' + ? OrderReportStat + : T extends 'taxes' + ? TaxesReportStat + : T extends 'coupons' + ? CouponsReportStat + : T extends 'customers' + ? CustomersReportStat + : never; + totalResults: number; + totalPages: number; +}; + +export type ReportState = { + itemErrors: { + [ resourceName: string ]: unknown; + }; + items: { + [ resourceName: string ]: ReportItemObject; + }; + statErrors: { + [ resourceName: string ]: unknown; + }; + stats: { + [ resourceName: string ]: ReportStatObject; + }; +}; diff --git a/packages/js/data/src/reports/utils.js b/packages/js/data/src/reports/utils.ts similarity index 79% rename from packages/js/data/src/reports/utils.js rename to packages/js/data/src/reports/utils.ts index fbe58240fba..065c4847653 100644 --- a/packages/js/data/src/reports/utils.js +++ b/packages/js/data/src/reports/utils.ts @@ -14,6 +14,7 @@ import { getQueryFromActiveFilters, } from '@woocommerce/navigation'; import deprecated from '@wordpress/deprecated'; +import { select as WPSelect } from '@wordpress/data'; /** * Internal dependencies @@ -22,46 +23,43 @@ import * as reportsUtils from './utils'; import { MAX_PER_PAGE, QUERY_DEFAULTS } from '../constants'; import { STORE_NAME } from './constants'; import { getResourceName } from '../utils'; +import { + ReportItemsEndpoint, + ReportStatEndpoint, + ReportStatObject, +} from './types'; +import type { ReportsSelect } from './'; -/** - * Add filters and advanced filters values to a query object. - * - * @param {Object} options arguments - * @param {string} options.endpoint Report API Endpoint - * @param {Object} options.query Query parameters in the url - * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. - * @param {Array} [options.filters] config filters - * @param {Object} [options.advancedFilters] config advanced filters - * @return {Object} A query object with the values from filters and advanced fitlters applied. - */ -export function getFilterQuery( options ) { - const { - endpoint, - query, - limitBy, - filters = [], - advancedFilters = {}, - } = options; - if ( query.search ) { - const limitProperties = limitBy || [ endpoint ]; - return limitProperties.reduce( ( result, limitProperty ) => { - result[ limitProperty ] = query[ limitProperty ]; - return result; - }, {} ); - } +type Filter = { + param: string; + filters: Array< Record< string, unknown > >; +}; - return filters - .map( ( filter ) => - getQueryFromConfig( filter, advancedFilters, query ) - ) - .reduce( - ( result, configQuery ) => Object.assign( result, configQuery ), - {} - ); -} +type AdvancedFilters = + | { + filters: { + [ key: string ]: { + input: { + component: string; + }; + }; + }; + } + | Record< string, never >; -// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response. -const noIntervalEndpoints = [ 'stock', 'customers' ]; +type QueryOptions = { + endpoint: ReportStatEndpoint; + dataType: 'primary' | 'secondary'; + query: Record< string, string >; + limitBy: string[]; + filters: Array< Filter >; + advancedFilters: AdvancedFilters; + defaultDateRange: string; + tableQuery: Record< string, string >; + fields: string[]; + selector: ReportsSelect; + select: typeof WPSelect; +}; /** * Add timestamp to advanced filter parameters involving date. The api @@ -71,7 +69,10 @@ const noIntervalEndpoints = [ 'stock', 'customers' ]; * @param {Object} activeFilter - an active filter. * @return {Object} - an active filter with timestamp added to date values. */ -export function timeStampFilterDates( config, activeFilter ) { +export function timeStampFilterDates( + config: AdvancedFilters, + activeFilter: ActiveFilter +) { const advancedFilterConfig = config.filters[ activeFilter.key ]; if ( get( advancedFilterConfig, [ 'input', 'component' ] ) !== 'Date' ) { return activeFilter; @@ -99,7 +100,11 @@ export function timeStampFilterDates( config, activeFilter ) { } ); } -export function getQueryFromConfig( config, advancedFilters, query ) { +export function getQueryFromConfig( + config: Filter, + advancedFilters: QueryOptions[ 'advancedFilters' ], + query: QueryOptions[ 'query' ] +) { const queryValue = query[ config.param ]; if ( ! queryValue ) { @@ -155,6 +160,59 @@ export function getQueryFromConfig( config, advancedFilters, query ) { }; } +/** + * Add filters and advanced filters values to a query object. + * + * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint + * @param {Object} options.query Query parameters in the url + * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. + * @param {Array} [options.filters] config filters + * @param {Object} [options.advancedFilters] config advanced filters + * @return {Object} A query object with the values from filters and advanced fitlters applied. + */ +export function getFilterQuery( + options: Omit< QueryOptions, 'endpoint' > & { + endpoint: ReportItemsEndpoint | ReportStatEndpoint; + } +) { + const { + endpoint, + query, + limitBy, + filters = [], + advancedFilters = {}, + } = options; + if ( query.search ) { + const limitProperties = limitBy || [ endpoint ]; + return limitProperties.reduce< Record< string, string > >( + ( result, limitProperty ) => { + result[ limitProperty ] = query[ limitProperty ]; + return result; + }, + {} + ); + } + + return filters + .map( ( filter ) => + getQueryFromConfig( filter, advancedFilters, query ) + ) + .reduce( + ( result, configQuery ) => Object.assign( result, configQuery ), + {} + ); +} + +// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response. +const noIntervalEndpoints = [ 'stock', 'customers' ] as const; + +type ActiveFilter = { + key: string; + rule: 'after' | 'before'; + value: string; +}; + /** * Returns true if a report object is empty. * @@ -162,7 +220,10 @@ export function getQueryFromConfig( config, advancedFilters, query ) { * @param {string} endpoint Endpoint slug * @return {boolean} True if report is data is empty. */ -export function isReportDataEmpty( report, endpoint ) { +export function isReportDataEmpty( + report: ReportStatObject, + endpoint: ReportStatEndpoint +) { if ( ! report ) { return true; } @@ -186,15 +247,17 @@ export function isReportDataEmpty( report, endpoint ) { /** * Constructs and returns a query associated with a Report data request. * - * @param {Object} options arguments - * @param {string} options.endpoint Report API Endpoint - * @param {string} options.dataType 'primary' or 'secondary'. - * @param {Object} options.query Query parameters in the url. - * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. - * @param {string} options.defaultDateRange User specified default date range. + * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint + * @param {string} options.dataType 'primary' or 'secondary'. + * @param {Object} options.query Query parameters in the url. + * @param {Array} [options.filters] config filters + * @param {Object} [options.advancedFilters] config advanced filters + * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. + * @param {string} options.defaultDateRange User specified default date range. * @return {Object} data request query parameters. */ -function getRequestQuery( options ) { +export function getRequestQuery( options: QueryOptions ) { const { endpoint, dataType, query, fields, defaultDateRange } = options; const datesFromQuery = getCurrentDates( query, defaultDateRange ); const interval = getIntervalForQuery( query, defaultDateRange ); @@ -222,15 +285,19 @@ function getRequestQuery( options ) { /** * Returns summary number totals needed to render a report page. * - * @param {Object} options arguments - * @param {string} options.endpoint Report API Endpoint - * @param {Object} options.query Query parameters in the url - * @param {Object} options.select Instance of @wordpress/select - * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. - * @param {string} options.defaultDateRange User specified default date range. + * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint + * @param {Object} options.query Query parameters in the url + * @param {Object} options.select Instance of @wordpress/select + * @param {Array} [options.filters] config filters + * @param {Object} [options.advancedFilters] config advanced filters + * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. + * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object containing summary number responses. */ -export function getSummaryNumbers( options ) { +export function getSummaryNumbers< T extends ReportStatEndpoint >( + options: QueryOptions +) { const { endpoint, select } = options; const { getReportStats, getReportStatsError, isResolving } = select( STORE_NAME ); @@ -248,7 +315,7 @@ export function getSummaryNumbers( options ) { // Disable eslint rule requiring `getReportStats` to be defined below because the next two statements // depend on `getReportStats` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const primary = getReportStats( endpoint, primaryQuery ); + const primary = getReportStats< T >( endpoint, primaryQuery ); if ( isResolving( 'getReportStats', [ endpoint, primaryQuery ] ) ) { return { ...response, isRequesting: true }; @@ -267,7 +334,7 @@ export function getSummaryNumbers( options ) { // Disable eslint rule requiring `getReportStats` to be defined below because the next two statements // depend on `getReportStats` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const secondary = getReportStats( endpoint, secondaryQuery ); + const secondary = getReportStats< T >( endpoint, secondaryQuery ); if ( isResolving( 'getReportStats', [ endpoint, secondaryQuery ] ) ) { return { ...response, isRequesting: true }; @@ -317,7 +384,7 @@ const reportChartDataResponses = { }, }; -const EMPTY_ARRAY = []; +const EMPTY_ARRAY = [] as const; /** * Cache helper for returning the full chart dataset after multiple @@ -325,7 +392,7 @@ const EMPTY_ARRAY = []; * all the requests have resolved successfully. */ const getReportChartDataResponse = memoize( - ( requestString, totals, intervals ) => ( { + ( _requestString, totals, intervals ) => ( { isEmpty: false, isError: false, isRequesting: false, @@ -348,7 +415,9 @@ const getReportChartDataResponse = memoize( * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object containing API request information (response, fetching, and error details) */ -export function getReportChartData( options ) { +export function getReportChartData< T extends ReportStatEndpoint >( + options: QueryOptions +) { const { endpoint } = options; let reportSelectors = options.selector; if ( options.select && ! options.selector ) { @@ -365,7 +434,7 @@ export function getReportChartData( options ) { // Disable eslint rule requiring `stats` to be defined below because the next two if statements // depend on `getReportStats` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const stats = getReportStats( endpoint, requestQuery ); + const stats = getReportStats< T >( endpoint, requestQuery ); if ( isResolving( 'getReportStats', [ endpoint, requestQuery ] ) ) { return reportChartDataResponses.requesting; @@ -394,7 +463,7 @@ export function getReportChartData( options ) { for ( let i = 2; i <= totalPages; i++ ) { const nextQuery = { ...requestQuery, page: i }; - const _data = getReportStats( endpoint, nextQuery ); + const _data = getReportStats< T >( endpoint, nextQuery ); if ( isResolving( 'getReportStats', [ endpoint, nextQuery ] ) ) { continue; } @@ -444,7 +513,10 @@ export function getReportChartData( options ) { * @param {Function} formatAmount format currency function * @return {string|Function} returns a number format based on the type or an overriding formatting function */ -export function getTooltipValueFormat( type, formatAmount ) { +export function getTooltipValueFormat( + type: string, + formatAmount: ( amount: number ) => string +) { switch ( type ) { case 'currency': return formatAmount; @@ -463,12 +535,17 @@ export function getTooltipValueFormat( type, formatAmount ) { * Returns query needed for a request to populate a table. * * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint * @param {Object} options.query Query parameters in the url * @param {Object} options.tableQuery Query parameters specific for that endpoint * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object Table data response */ -export function getReportTableQuery( options ) { +export function getReportTableQuery( + options: Omit< QueryOptions, 'endpoint' > & { + endpoint: ReportItemsEndpoint; + } +) { const { query, tableQuery = {} } = options; const filterQuery = getFilterQuery( options ); const datesFromQuery = getCurrentDates( query, options.defaultDateRange ); @@ -503,7 +580,11 @@ export function getReportTableQuery( options ) { * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object Table data response */ -export function getReportTableData( options ) { +export function getReportTableData< T extends ReportItemsEndpoint >( + options: Omit< QueryOptions, 'endpoint' > & { + endpoint: ReportItemsEndpoint; + } +) { const { endpoint } = options; let reportSelectors = options.selector; if ( options.select && ! options.selector ) { @@ -530,7 +611,7 @@ export function getReportTableData( options ) { // Disable eslint rule requiring `items` to be defined below because the next two if statements // depend on `getReportItems` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const items = getReportItems( endpoint, tableQuery ); + const items = getReportItems< T >( endpoint, tableQuery ); const queryResolved = hasFinishedResolution( 'getReportItems', [ endpoint, From f10f740958c29ad41b60dde7cf13515caad112df Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Fri, 28 Oct 2022 05:42:46 -0700 Subject: [PATCH 089/149] Fix release post not parsing certain versions correctly (#35363) --- .../commands/release-post/release-post-release.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/release-posts/commands/release-post/release-post-release.ts b/tools/release-posts/commands/release-post/release-post-release.ts index 60308f82ffd..7dda60cf4fa 100644 --- a/tools/release-posts/commands/release-post/release-post-release.ts +++ b/tools/release-posts/commands/release-post/release-post-release.ts @@ -51,7 +51,14 @@ const program = new Command() if ( ! options.previousVersion && previousVersion ) { // e.g 6.8.0 -> 6.7.0 - previousVersion.minor -= 1; + previousVersion.major = + previousVersion.minor === 0 + ? previousVersion.major - 1 + : previousVersion.major; + + previousVersion.minor = + previousVersion.minor === 0 ? 9 : previousVersion.minor - 1; + previousVersion.format(); } From 12b79d32c2fde6e2a57236e071ce54f7235640d8 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Fri, 28 Oct 2022 09:32:28 -0400 Subject: [PATCH 090/149] DateTimePickerControl: Allow time to be set to beginning or end of day when in date-only mode (#35296) --- ...ate-date-time-picker-control-force-time-to | 4 + .../date-time-picker-control.tsx | 40 +++++- .../stories/index.tsx | 7 ++ .../date-time-picker-control/test/index.tsx | 116 ++++++++++++++++++ 4 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 packages/js/components/changelog/update-date-time-picker-control-force-time-to diff --git a/packages/js/components/changelog/update-date-time-picker-control-force-time-to b/packages/js/components/changelog/update-date-time-picker-control-force-time-to new file mode 100644 index 00000000000..fcf4278fc5f --- /dev/null +++ b/packages/js/components/changelog/update-date-time-picker-control-force-time-to @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added ability to force time when DateTimePickerControl is date-only (timeForDateOnly prop). diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index 57626549bf1..b53a34d9ed7 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -40,6 +40,7 @@ export type DateTimePickerControlProps = { disabled?: boolean; isDateOnlyPicker?: boolean; is12HourPicker?: boolean; + timeForDateOnly?: 'start-of-day' | 'end-of-day'; onChange?: DateTimePickerControlOnChangeHandler; onBlur?: () => void; label?: string; @@ -52,6 +53,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { currentDate, isDateOnlyPicker = false, is12HourPicker = true, + timeForDateOnly = 'start-of-day', dateTimeFormat, disabled = false, onChange, @@ -120,6 +122,20 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { return formatDate( displayFormat, momentDate.local() ); } + function maybeForceTime( momentDate: Moment ): Moment { + if ( ! isDateOnlyPicker ) return momentDate; + + const updatedMomentDate = momentDate.clone(); + + if ( timeForDateOnly === 'start-of-day' ) { + updatedMomentDate.startOf( 'day' ); + } else if ( timeForDateOnly === 'end-of-day' ) { + updatedMomentDate.endOf( 'day' ); + } + + return updatedMomentDate; + } + function hasFocusLeftInputAndDropdownContent( event: React.FocusEvent< HTMLInputElement > ): boolean { @@ -165,15 +181,17 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { onChangePropFunctionRef.current = onChange; }, [ onChange ] ); - const inputStringChangeHandlerFunctionRef = useRef< - ( newInputString: string, fireOnChange: boolean ) => void - >( ( newInputString: string, fireOnChange: boolean ) => { + function inputStringChangeHandlerFunction( + newInputString: string, + fireOnChange: boolean + ) { if ( ! isMounted.current ) return; - const newDateTime = parseMoment( newInputString ); + let newDateTime = parseMoment( newInputString ); const isValid = newDateTime.isValid(); if ( isValid ) { + newDateTime = maybeForceTime( newDateTime ); setLastValidDate( newDateTime ); } @@ -186,7 +204,17 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { isValid ); } - } ); + } + + const inputStringChangeHandlerFunctionRef = useRef< + ( newInputString: string, fireOnChange: boolean ) => void + >( inputStringChangeHandlerFunction ); + // whenever forceTimeTo changes, we need to reset the ref to inputStringChangeHandlerFunction + // so that we are using the most current forceTimeTo value inside of it + useEffect( () => { + inputStringChangeHandlerFunctionRef.current = + inputStringChangeHandlerFunction; + }, [ timeForDateOnly ] ); const debouncedInputStringChangeHandler = useDebounce( inputStringChangeHandlerFunctionRef.current, @@ -232,7 +260,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } else { changeImmediate( currentDate || '', fireOnChange ); } - }, [ currentDate, displayFormat ] ); + }, [ currentDate, displayFormat, timeForDateOnly ] ); return ( { ); }, 10000 ); + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should force time to the start of the day if date only', async () => { + const originalDateTime = moment( '09-15-2022' ); + const newDateTimeInputString = '06-08-2010'; + const newDateTime = moment( newDateTimeInputString ).startOf( 'day' ); + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( + () => + expect( onChangeHandler ).toHaveBeenLastCalledWith( + newDateTime.toISOString(), + true + ), + { timeout: 100 } + ); + }, 10000 ); + + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should force time to the end of the day if date only', async () => { + const originalDateTime = moment( '09-15-2022' ); + const newDateTimeInputString = '06-08-2010'; + const newDateTime = moment( newDateTimeInputString ).endOf( 'day' ); + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( + () => + expect( onChangeHandler ).toHaveBeenLastCalledWith( + newDateTime.toISOString(), + true + ), + { timeout: 100 } + ); + }, 10000 ); + + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should not force time to the start of the day if not date only', async () => { + const originalDateTime = moment( '09-15-2022' ); + const newDateTimeInputString = '06-08-2010 7:00'; + const newDateTime = moment( newDateTimeInputString ); + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( + () => + expect( onChangeHandler ).toHaveBeenLastCalledWith( + newDateTime.toISOString(), + true + ), + { timeout: 100 } + ); + }, 10000 ); + // We need to bump up the timeout for this test because: // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) // 2. moment.js is slow From 0f204dbb575fed81d9a0ed8befc0e83a5b992cfa Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 28 Oct 2022 18:01:40 +0200 Subject: [PATCH 091/149] TT3 compatibility (#35306) --- plugins/woocommerce/changelog/add-tt3-support | 4 + .../legacy/css/twenty-twenty-three.scss | 1174 +++++++++++++++++ .../client/legacy/css/twenty-twenty-two.scss | 49 + .../legacy/css/woocommerce-blocktheme.scss | 24 +- .../client/legacy/css/woocommerce.scss | 2 + .../woocommerce/includes/class-wc-cart.php | 9 +- .../includes/class-woocommerce.php | 3 + .../class-wc-twenty-twenty-three.php | 82 ++ .../includes/wc-cart-functions.php | 5 +- .../includes/wc-conditional-functions.php | 21 +- .../includes/wc-core-functions.php | 1 + .../includes/wc-template-functions.php | 8 +- .../woocommerce/templates/auth/form-login.php | 4 +- .../woocommerce/templates/cart/cart-empty.php | 4 +- plugins/woocommerce/templates/cart/cart.php | 6 +- .../cart/proceed-to-checkout-button.php | 4 +- .../templates/cart/shipping-calculator.php | 4 +- .../templates/checkout/form-coupon.php | 4 +- .../templates/checkout/form-pay.php | 4 +- .../templates/checkout/payment.php | 6 +- .../templates/content-widget-price-filter.php | 4 +- .../templates/global/form-login.php | 4 +- .../myaccount/form-add-payment-method.php | 4 +- .../templates/myaccount/form-edit-account.php | 4 +- .../templates/myaccount/form-edit-address.php | 4 +- .../templates/myaccount/form-login.php | 6 +- .../myaccount/form-lost-password.php | 4 +- .../myaccount/form-reset-password.php | 4 +- .../templates/myaccount/orders.php | 4 +- .../templates/order/form-tracking.php | 4 +- .../templates/product-searchform.php | 4 +- .../single-product/add-to-cart/external.php | 4 +- .../single-product/add-to-cart/grouped.php | 4 +- .../single-product/add-to-cart/simple.php | 4 +- .../variation-add-to-cart-button.php | 4 +- .../legacy/unit-tests/cart/functions.php | 41 +- 36 files changed, 1423 insertions(+), 98 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-tt3-support create mode 100644 plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss create mode 100644 plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php diff --git a/plugins/woocommerce/changelog/add-tt3-support b/plugins/woocommerce/changelog/add-tt3-support new file mode 100644 index 00000000000..aeeb9d06453 --- /dev/null +++ b/plugins/woocommerce/changelog/add-tt3-support @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Twenty Twenty-Three theme compatibility. diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss new file mode 100644 index 00000000000..862f78ea0ab --- /dev/null +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss @@ -0,0 +1,1174 @@ +/** +* Fonts +*/ +@font-face { + font-family: star; + src: url(../fonts/star.eot); + src: + url(../fonts/star.eot?#iefix) format("embedded-opentype"), + url(../fonts/star.woff) format("woff"), + url(../fonts/star.ttf) format("truetype"), + url(../fonts/star.svg#star) format("svg"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: WooCommerce; + src: url(../fonts/WooCommerce.eot); + src: + url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), + url(../fonts/WooCommerce.woff) format("woff"), + url(../fonts/WooCommerce.ttf) format("truetype"), + url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); + font-weight: 400; + font-style: normal; +} + +@import "mixins"; +@import "animation"; + + +/* + Layout fix. + */ +.woocommerce-page { + + main { + // This is to allow .woocommerce div to have width of 1000px on styles with full width layout (such as Pitch). + max-width: calc(1000px + var(--wp--style--root--padding-right) + var(--wp--style--root--padding-left)); + margin-left: auto; + margin-right: auto; + + .woocommerce { + @include clearfix(); + } + } +} + +.theme-twentytwentythree { + .container-colors { + display: flex; + flex-direction: row; + } + + .cube { + width: 20%; + height: 100px; + text-align: center; + vert-align: middle; + } + + .base { + background-color: var(--wp--preset--color--base); + } + + .contrast { + background-color: var(--wp--preset--color--contrast); + color: var(--wp--preset--color--base); + } + + .primary { + background-color: var(--wp--preset--color--primary); + } + + .secondary { + background-color: var(--wp--preset--color--secondary); + } + + .tertiary { + background-color: var(--wp--preset--color--tertiary); + } + + +} + +.woocommerce { + + /* + Common/global + */ + + // Make quantity selector less wide. + .quantity { + input[type="number"] { + width: 3em; + } + + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + opacity: 1; + } + } + + // Breadcrumbs are unnecessary on the shop page. + &.woocommerce-shop .woocommerce-breadcrumb { + display: none; + } + + // Make sure breadcrumbs are not overlapping with Sale badge on the Single product page, etc. + .woocommerce-breadcrumb { + margin-bottom: 1rem; + } + + /* + Notice messages (like 'Added to cart', 'Billing address needs to be filled in', etc. + */ + .woocommerce-message, + .woocommerce-error, + .woocommerce-info { + background-color: rgba(176, 176, 176, 0.6); + color: #222; + border-top-color: var(--wp--preset--color--primary); + border-top-style: solid; + border-top-width: 2px; + padding: 1rem 1.5rem; + margin-bottom: 2rem; + list-style: none; + font-size: var(--wp--preset--font-size--small); + display: flow-root; + + &[role="alert"]::before { + background: #d5d5d5; + color: black; + border-radius: 5rem; + font-size: 1rem; + padding-left: 3px; + padding-right: 3px; + margin-right: 1rem; + } + + a { + color: var(--wp--preset--color--contrast); + + .button { + margin-top: -0.5rem; + border: none; + padding: 0.5rem 1rem; + } + } + } + + .woocommerce-error[role="alert"] { + margin: 0; + + &::before { + content: "X"; + padding-right: 4px; + padding-left: 4px; + } + + li { + display: inline-block; + } + } + + .woocommerce-message { + &[role="alert"]::before { + content: "\2713"; + } + } + + // Checkout notice group styling. + .woocommerce-NoticeGroup-checkout { + ul.woocommerce-error[role="alert"] { + color: var(--wp--preset--color--contrast); + background: var(--wp--preset--color--primary); + + &::before { + display: none; + } + li { + display: inherit; + margin-bottom: 1rem; + } + } + } + + /* + Shop page. + */ + + // Styling the buttons on the Shop page. + a.button, + button[name="add-to-cart"], + input[name="submit"], + button.single_add_to_cart_button, + button[type="submit"]:not(.wp-block-search__button) { + display: inline-block; + text-align: center; + word-break: break-word; + padding: 1rem 2rem; + margin-top: 1rem; + text-decoration: none; + font-size: medium; + cursor: pointer; + + } + + // Style the 'Showing A-B of X results' text. + .woocommerce-result-count { + margin-top: 0; + } + + // The 'order by' dropdown on the Shop page is rather tiny unless the font size is increased. + select.orderby { + font-size: var(--wp--preset--font-size--medium); + } + + // Products. + ul.products { + + padding-inline-start: 0; + display: flex; + align-items: stretch; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + @media only screen and (max-width: 768px) { + justify-content: space-between; + } + + li.product { + list-style: none; + margin-top: var(--wp--style--block-gap); + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + + a.woocommerce-loop-product__link { + text-decoration: none; + display: block; + border: 0; + } + + h2.woocommerce-loop-product__title { + color: var(--wp--preset--color--contrast); + font-family: var(--wp--preset--font-family--system-font); + text-decoration: none; + margin-bottom: 0; + } + + h2.woocommerce-loop-category__title { + font-size: revert; + } + + // Add to cart/Select options/Read more buttons. + a.button { + padding: 0.8rem 10%; + margin-left: auto; + margin-right: auto; + + &.loading { + opacity: 0.5; + } + } + + // View cart link. + a.added_to_cart { + margin: 1rem auto; + } + + } + } + + // Position page numbers under list of products horizontally in the centre of the page. + ul.page-numbers { + text-align: center; + } + + // On sale badge. + span.onsale { + top: -1rem; + right: -1rem; + position: absolute; + background: var(--wp--preset--color--tertiary); + color: var(--wp--preset--color--contrast); + border-radius: 2rem; + line-height: 2.6rem; + font-size: 0.8rem; + padding: 0 0.5rem 0 0.5rem; + } + + /* + Single product page. + */ + + div.product { + position: relative; + + > span.onsale { + position: absolute; + left: -1rem; + top: -1rem; + width: 1.8rem; + z-index: 1; + } + + // How price gets displayed + .entry-summary { + .woocommerce-Price-amount, + del, + .price { + font-size: var(--wp--preset--font-size--large); + } + } + + div.woocommerce-product-gallery { + position: relative; + + a.woocommerce-product-gallery__trigger { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 1; + text-decoration: none; + border-radius: 1rem; + border-style: solid; + line-height: 1.5rem; + padding: 0; + font-size: 0.6rem; + background: var(--wp--preset--color--white); + border-color: var(--wp--preset--color--white); + height: 1.5rem; + width: 1.5rem; + overflow: hidden; + + &::before { + content: url('data:image/svg+xml;utf8,'); + display: block; + transform: rotateY(180deg); + margin-left: 1.55rem; + } + } + + figure.woocommerce-product-gallery__wrapper { + margin: 0; + } + + } + + div.summary { + font-size: 1rem; + + h1.product_title { + font-size: var(--wp--preset--font-size--huge); + margin: 0; + } + + figure.woocommerce-product-gallery__wrapper { + margin: 0; + } + + .woocommerce-product-rating { + .star-rating { + display: inline-block; + } + .woocommerce-review-link { + display: inline-block; + overflow: hidden; + position: relative; + top: -0.5em; + font-size: 1em; + } + } + + .quantity { + display: inline-block; + } + } + + table.variations tr { + + display: table-row; + margin-bottom: 0; + text-align: left; + + td select { + margin: calc(var(--wp--style--block-gap) / 4) 0; + } + } + + .single_variation_wrap { + margin-top: var(--wp--style--block-gap); + } + + button[name="add-to-cart"], + button.single_add_to_cart_button { + margin-top: 0.5rem; + margin-bottom: var(--wp--style--block-gap); + } + + ol.flex-control-thumbs { + padding-left: 0; + float: left; + + li { + list-style: none; + cursor: pointer; + float: left; + width: 18%; + margin-right: 1rem; + } + + } + + a.reset_variations { + margin-left: 0.5em; + } + + table.group_table { + td { + padding-right: 0.5rem; + padding-bottom: 1rem; + } + + margin-bottom: var(--wp--style--block-gap); + } + + .related.products { + margin-top: 7rem; + } + + } + + // Description/Additional info/Reviews tabs. + .woocommerce-tabs { + padding-top: var(--wp--style--block-gap); + + ul.wc-tabs { + padding: 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-bottom-color: #eae9eb; + + li { + opacity: 0.5; + color: var(--wp--preset--color--contrast); + margin: 0; + padding: 0.5em 1em 0.5em 1em; + border-color: #eae9eb; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + float: left; + border-style: solid; + border-width: 1px; + font-weight: 600; + font-size: var(--wp--preset--font-size--medium); + + &:first-child { + border-left-color: #eae9eb; + margin-left: 1em; + } + + &.active { + background: var(--wp--preset--color--tertiary); + color: var(--wp--preset--color--contrast); + opacity: 1; + } + + a { + text-decoration: none; + color: var(--wp--preset--color--contrast); + } + } + } + + .woocommerce-Tabs-panel { + padding-top: var(--wp--style--block-gap); + font-size: var(--wp--preset--font-size--small); + margin-left: 1em; + + h2 { + display: none; + } + + table.woocommerce-product-attributes { + text-align: left; + } + } + } + + // Reviews tab. + .woocommerce-Reviews { + ol.commentlist { + list-style: none; + padding-left: 0; + + li { + margin-bottom: var(--wp--style--block-gap); + } + + img.avatar { + float: left; + } + + p.meta { + font-size: 1rem; + } + + .comment-text { + display: flow-root; + padding-left: var(--wp--style--block-gap); + + .star-rating { + margin-top: 0; + margin-right: unset; + margin-left: unset; + } + } + } + + .comment-form-rating { + label { + display: inline-block; + padding-right: var(--wp--style--block-gap); + padding-top: var(--wp--style--block-gap); + } + + p.stars { + display: inline; + a::before { + color: var(--wp--preset--color--contrast); + } + } + } + + .comment-form-comment { + label { + float: left; + padding-right: var(--wp--style--block-gap); + } + } + + #review_form_wrapper { + margin-top: 5rem; + } + } + + // Rating: show stars instead of 1, 2, 3, 4, 5. + .star-rating { + overflow: hidden; + position: relative; + height: 1em; + line-height: 1; + width: 5.4rem; + font-family: star; + font-style: normal; + color: var(--wp--preset--color--contrast); + margin: 1rem auto 0.7rem auto; + + &::before { + content: "\73\73\73\73\73"; + float: left; + top: 0; + left: 0; + position: absolute; + font-size: 1rem; + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + font-size: 1rem; + } + } + + // Rating stars. + p.stars { + margin-top: 0; + + a { + position: relative; + height: 1em; + width: 1em; + text-indent: -999em; + display: inline-block; + text-decoration: none; + box-shadow: none; + font-style: normal; + + &::before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 1em; + height: 1em; + line-height: 1; + font-family: WooCommerce; + content: "\e021"; + text-indent: 0; + } + + &:hover { + + ~ a::before { + content: "\e021"; + } + } + } + + &:hover { + + a { + + &::before { + content: "\e020"; + } + } + } + + &.selected { + + a.active { + + &::before { + content: "\e020"; + } + + ~ a::before { + content: "\e021"; + } + } + + a:not(.active) { + + &::before { + content: "\e020"; + } + } + } + } + + // Highlights for specific info, e.g. on the My Account > Orders > Order X: Order ___ was placed on ____ + mark { + font-weight: bold; + background-color: transparent; + } + +} + +.woocommerce-page { + .woocommerce-cart-form { + + // Make coupon code input less ginormous. + #coupon_code { + padding: 0 1rem; + } + + .actions { + button.button { + height: initial; + } + } + + // Cart table, aka review of cart items. + table.shop_table_responsive { + + td, + th { + padding: 1rem 0 0.5rem 1rem; + } + + tbody { + + tr:last-of-type { + border-top: none; + } + + @media only screen and (max-width: 768px) { + td { + padding-left: 0; + } + + .product-remove { + text-align: left !important; + } + + #coupon_code { + float: left; + margin-bottom: 1rem; + } + } + } + + .product-remove { + font-size: var(--wp--preset--font-size--large); + + a { + text-decoration: none; + } + } + } + } + + // Elements around "Proceed to Checkout" button. + .cart-collaterals { + margin-top: 1.5rem; + + h2 { + text-transform: uppercase; + font-family: inherit; + } + + table.shop_table_responsive { + + tr { + border-top: none; + } + + th { + width: 11rem; + } + + td, + th { + padding: 1rem 0; + vertical-align: text-top; + } + } + + button[name="calc_shipping"] { + padding: 1rem 2rem; + } + + .woocommerce-Price-amount { + font-weight: normal; + } + } + + // Style the payment gateway selection input--improve the size of the click target, etc + input[type="radio"][name="payment_method"], + input[type="radio"].shipping_method { + display: none; + + & + label { + + &::before { + content: ""; + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid var(--wp--preset--color--black); + background: var(--wp--preset--color--white); + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(0.2rem); + } + } + + & ~ .payment_box { + padding-left: 3rem; + margin-top: 1rem; + } + + &:checked + label { + + &::before { + background: radial-gradient(circle at center, black 45%, white 0); + } + } + } + + // Style labels like "Remember me?" or "Ship to different address". + label.woocommerce-form__label-for-checkbox { + font-weight: normal; + cursor: pointer; + + span { + + &::before { + content: ""; + display: inline-block; + height: 1rem; + width: 1rem; + border: 2px solid var(--wp--preset--color--black); + background: var(--wp--preset--color--white); + margin-right: 0.5rem; + transform: translateY(0.2rem); + } + } + + input[type="checkbox"] { + display: none; + } + + input[type="checkbox"]:checked + span::before { + background: var(--wp--preset--color--black); + box-shadow: inset 0.2rem 0.2rem var(--wp--preset--color--white), inset -0.2rem -0.2rem var(--wp--preset--color--white); + } + } + + // Cart totals, Cart page table or Order list in My Account. + table.shop_table_responsive { + text-align: left; + + th, + td { + font-size: var(--wp--preset--font-size--small); + font-weight: normal; + } + + th { + padding-bottom: 1rem; + } + + tbody { + + tr { + border-top: 1px solid var(--wp--preset--color--contrast); + } + + td { + a.button, + button { + margin-bottom: 1rem; + padding: 0.5rem 1rem 0.5rem 1rem; + } + + &.woocommerce-orders-table__cell-order-actions { + a.button { + display: block; + + @media only screen and (max-width: 768px) { + width: 50%; + margin-left: auto; + } + } + } + } + } + } + + table.shop_table, + table.shop_table_responsive { + tbody { + .product-name { + + .variation { + dt { + font-style: italic; + margin-right: 0.25rem; + float: left; + } + + dd { + font-style: normal; + + a { + font-style: normal; + } + } + } + } + } + + td, + th { + padding: 0.5rem; + } + } + + // Improve styling of the 'Have a coupon?' section on the checkout page. + form.checkout_coupon { + padding-left: 1.5rem; + // 1.5 rem is to account for extra padding we added above. + width: calc(100% - 1.5rem); + + .form-row { + button[name="apply_coupon"] { + margin-top: 0; + } + } + } + + // Hide the dot to the left of list items as we style the checkboxes: Shipping method and Payment method selection. + ul.wc_payment_methods, + ul.woocommerce-shipping-methods { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; + padding-left: 0; + + input[type="radio"] { + margin-right: 0.6rem; + } + + li.wc_payment_method { + margin-bottom: 1rem; + } + } + + // Layout of the checkout: Billing vs Shipping address, Cart overview, etc. + .woocommerce-checkout, + &.woocommerce-order-pay { + display: table; + + h3 { + font-family: inherit; + font-size: var(--wp--preset--font-size--large); + font-weight: 700; + } + + .col2-set { + width: 43%; + float: right; + } + + .blockUI.blockOverlay { + position: relative; + @include loader(); + } + + #customer_details { + width: 53%; + float: left; + + .col-1, + .col-2 { + width: 100%; + float: none; + } + } + + @media only screen and (max-width: 768px) { + .col2-set, + #customer_details { + width: 100%; + float: none; + } + } + + .woocommerce-billing-fields__field-wrapper, + .woocommerce-checkout-review-order-table, + .woocommerce-checkout-payment, + #payment { + margin-top: 4rem; + } + + .woocommerce-checkout-review-order-table, + #order_review .shop_table { + border-collapse: collapse; + width: 100%; + + thead { + display: none; + } + + th { + text-align: left; + font-weight: normal; + } + + th, + td { + padding: 1rem 1rem 1rem 0; + vertical-align: text-top; + } + + tbody { + border-bottom: 1px solid #d2ced2; + } + + tr.order-total { + border-top: 1px solid #d2ced2; + } + + .product-quantity { + font-weight: normal; + } + + .product-total, + .cart-subtotal, + .order-total, + .tax-rate, + input[type="radio"].shipping_method:checked + label, + input[type="hidden"].shipping_method + label { + .woocommerce-Price-amount { + font-weight: bold; + } + } + } + + button#place_order { + width: 100%; + text-transform: uppercase; + } + } + + /* + Thank you page (after checkout). + */ + + // Adds a tiny bit of vertical spacing on the Thank you (after checkout) page. + .woocommerce-order > * { + margin-bottom: var(--wp--style--block-gap); + } + + // Improves the presentation of order overview (order #, date, email, etc, not the line items) on the Thank you page. + ul.woocommerce-order-overview { + + li { + display: inline; + text-transform: uppercase; + + strong { + text-transform: none; + display: block; + } + } + } + + // Bottom section of the Thank you page---customer details: align, add border, make it look nice. + .woocommerce-customer-details { + address { + border: 1px solid var(--wp--preset--color--black); + font-style: inherit; + + p[class^="woocommerce-customer-details--"] { + &:first-of-type { + margin-top: 2rem; + } + + margin-top: 1rem; + margin-bottom: 0; + } + + .woocommerce-customer-details--phone::before { + content: "\01F4DE"; + margin-right: 1rem; + } + + .woocommerce-customer-details--email::before { + content: "\2709"; + margin-right: 1rem; + font-size: 1.8rem; + } + } + } + + // Better styling for Order line items on the Thank you page: create a table and align it to the left side. + .woocommerce-table--order-details { + border: 1px solid var(--wp--preset--color--black); + + th, + td { + text-align: left; + border-top: 1px solid var(--wp--preset--color--black); + border-bottom: 1px solid var(--wp--preset--color--black); + font-weight: normal; + } + + thead th { + text-transform: uppercase; + } + } +} + +/* + My Account + */ + +.woocommerce-account { + + // Make sure the floated content of My Account section doesn't overlap with the footer. + .woocommerce { + overflow: auto; + + table.woocommerce-table--order-downloads, + table.woocommerce-MyAccount-orders { + thead tr { + border-top: 2px solid var(--wp--preset--color--contrast); + + span { + font-weight: bold; + } + + } + + tbody a.button { + margin: calc(var(--wp--style--block-gap) / 6) 0; + } + } + + .woocommerce-MyAccount-navigation li { + &.is-active a { + &::before { + content: "> "; + } + } + + a { + text-decoration: initial; + + &:hover { + text-decoration: initial; + } + } + } + + + } + + // Make the Log in form on My Account page less wide. + .woocommerce-form-login { + max-width: 516px; + margin: 0 auto; + } + +} + +// TODO: This could look nicer. +.select2-container { + + .select2-selection, + .select2-dropdown { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + } + + .select2-dropdown { + border-top: 0; + + .select2-search__field { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + } + } +} + + +// Store-wide notice +.theme-twentytwentythree .woocommerce-store-notice { + color: black; + border-top: 2px solid var(--wp--preset--color--primary); + background: lightgray; + padding: 2rem; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + z-index: 999; + margin: 0; + + .woocommerce-store-notice__dismiss-link { + float: right; + margin-right: 4rem; + color: inherit; + } +} diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss index 7b1b7a888a3..37e8d7a01c0 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss @@ -27,6 +27,7 @@ @import "mixins"; @import "animation"; +@import "variables"; $tt2-gray: #f7f7f7; @@ -187,6 +188,29 @@ $tt2-gray: #f7f7f7; } } + // Moved from blocktheme.css to make sure TT2 won't be changed. + #respond input#submit, + input.button { + // Style primary WooCommerce CTAs in theme colors by default. + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + + &:hover { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + } + + &.disabled, + &:disabled, + &:disabled[disabled], + &.disabled:hover, + &:disabled:hover, + &:disabled[disabled]:hover { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + } + } + #respond input#submit.alt, a.button.alt, button.button.alt, @@ -220,6 +244,23 @@ $tt2-gray: #f7f7f7; padding: 1.5rem 3.5rem; } + // Moved from blockthemes.css to make sure TT2 won't be changed. + button.button, + a.button { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + + &.disabled, + &:disabled, + &:disabled[disabled], + &.disabled:hover, + &:disabled:hover, + &:disabled[disabled]:hover { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + } + } + // Shop page .woocommerce-result-count { @@ -420,6 +461,14 @@ $tt2-gray: #f7f7f7; } } + // Moved from blocktheme.scss to retain full styling. + ul.tabs li.active { + // Style active tab in theme colors. + background: var(--wp--preset--color--background, $contentbg); + border-bottom-color: var(--wp--preset--color--background, $contentbg); + } + + .woocommerce-Tabs-panel { padding-top: var(--wp--style--block-gap); font-size: var(--wp--preset--font-size--small); diff --git a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss index 00eabcdf3f4..017ba81fb1f 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss @@ -17,16 +17,14 @@ } } +.clear { + clear: both; +} + /** * General */ .woocommerce { - mark { - // Style the mark element in theme colors. - // For details see https://github.com/woocommerce/woocommerce/pull/31631. - background-color: var(--wp--preset--color--foreground, $highlight); - color: var(--wp--preset--color--background, $highlightext); - } /** * Buttons @@ -42,8 +40,6 @@ button.button, a.button { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); &.disabled, &:disabled, @@ -51,8 +47,6 @@ &.disabled:hover, &:disabled:hover, &:disabled[disabled]:hover { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); opacity: 0.5; } } @@ -60,13 +54,8 @@ #respond input#submit, input.button, a.button.alt { - // Style primary WooCommerce CTAs in theme colors by default. - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); &:hover { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); opacity: 0.9; } @@ -76,8 +65,6 @@ &.disabled:hover, &:disabled:hover, &:disabled[disabled]:hover { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); opacity: 0.5; } } @@ -114,9 +101,6 @@ .woocommerce-tabs { ul.tabs li.active { - // Style active tab in theme colors. - background: var(--wp--preset--color--background, $contentbg); - border-bottom-color: var(--wp--preset--color--background, $contentbg); &::before { box-shadow: 2px 2px 0 var(--wp--preset--color--background, $contentbg); diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss index da71b3321ed..efa4f5432d6 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss @@ -329,6 +329,7 @@ p.demo_store, li { border: 1px solid darken($secondary, 10%); background-color: $secondary; + color: $secondarytext; display: inline-block; position: relative; z-index: 0; @@ -351,6 +352,7 @@ p.demo_store, &.active { background: $contentbg; + color: $secondarytext; z-index: 2; border-bottom-color: $contentbg; diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php index 0b21b065092..c9f478342b1 100644 --- a/plugins/woocommerce/includes/class-wc-cart.php +++ b/plugins/woocommerce/includes/class-wc-cart.php @@ -1159,9 +1159,10 @@ class WC_Cart extends WC_Legacy_Cart { * @param string $message Message. * @param WC_Product $product_data Product data. */ - $message = apply_filters( 'woocommerce_cart_product_cannot_add_another_message', $message, $product_data ); + $message = apply_filters( 'woocommerce_cart_product_cannot_add_another_message', $message, $product_data ); + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; - throw new Exception( sprintf( '%s %s', wc_get_cart_url(), __( 'View cart', 'woocommerce' ), $message ) ); + throw new Exception( sprintf( '%s %s', wc_get_cart_url(), esc_attr( $wp_button_class ), __( 'View cart', 'woocommerce' ), $message ) ); } } @@ -1220,10 +1221,12 @@ class WC_Cart extends WC_Legacy_Cart { if ( isset( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] ) && ! $product_data->has_enough_stock( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] + $quantity ) ) { $stock_quantity = $product_data->get_stock_quantity(); $stock_quantity_in_cart = $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ]; + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; $message = sprintf( - '%s %s', + '%s %s', wc_get_cart_url(), + esc_attr( $wp_button_class ), __( 'View cart', 'woocommerce' ), /* translators: 1: quantity in stock 2: current quantity */ sprintf( __( 'You cannot add that amount to the cart — we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ), wc_format_stock_quantity_for_display( $stock_quantity_in_cart, $product_data ) ) diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 7783608ad42..34dbc0160c9 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -601,6 +601,9 @@ final class WooCommerce { case 'twentytwentytwo': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-two.php'; break; + case 'twentytwentythree': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-three.php'; + break; } } } diff --git a/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php new file mode 100644 index 00000000000..eea85f4ca61 --- /dev/null +++ b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php @@ -0,0 +1,82 @@ + 450, + 'single_image_width' => 600, + ) + ); + + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-three.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_twenty_three_styles', $styles ); + } + + /** + * Wrap checkout order review with a `col2-set` div. + */ + public static function before_order_review() { + echo '
'; + } + + /** + * Close the div wrapper. + */ + public static function after_order_review() { + echo '
'; + } +} + +WC_Twenty_Twenty_Three::init(); diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php index 1cab675f281..94fba81e7fd 100644 --- a/plugins/woocommerce/includes/wc-cart-functions.php +++ b/plugins/woocommerce/includes/wc-cart-functions.php @@ -118,11 +118,12 @@ function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) $added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) ); // Output success messages. + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { $return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) ); - $message = sprintf( '%s %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); + $message = sprintf( '%s %s', esc_url( $return_to ), esc_attr( $wp_button_class ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); } else { - $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); + $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_attr( $wp_button_class ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); } if ( has_filter( 'wc_add_to_cart_message' ) ) { diff --git a/plugins/woocommerce/includes/wc-conditional-functions.php b/plugins/woocommerce/includes/wc-conditional-functions.php index 780f310095e..aa9d3d522b4 100644 --- a/plugins/woocommerce/includes/wc-conditional-functions.php +++ b/plugins/woocommerce/includes/wc-conditional-functions.php @@ -500,7 +500,7 @@ function wc_is_file_valid_csv( $file, $check_path = true ) { /** * Check if the current theme is a block theme. * - * @since x.x.x + * @since 6.0.0 * @return bool */ function wc_current_theme_is_fse_theme() { @@ -517,10 +517,27 @@ function wc_current_theme_is_fse_theme() { /** * Check if the current theme has WooCommerce support or is a FSE theme. * - * @since x.x.x + * @since 6.0.0 * @return bool */ function wc_current_theme_supports_woocommerce_or_fse() { return (bool) current_theme_supports( 'woocommerce' ) || wc_current_theme_is_fse_theme(); } +/** + * Given an element name, returns a class name. + * + * If the WP-related function is not defined, return empty string. + * + * @param string $element The name of the element. + * + * @since 7.1.0 + * @return string + */ +function wc_wp_theme_get_element_class_name( $element ) { + if ( function_exists( 'wp_theme_get_element_class_name' ) ) { + return wp_theme_get_element_class_name( $element ); + } + + return ''; +} diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php index 726d681b172..63e8ee2e2b5 100644 --- a/plugins/woocommerce/includes/wc-core-functions.php +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -2397,6 +2397,7 @@ function wc_is_active_theme( $theme ) { function wc_is_wp_default_theme_active() { return wc_is_active_theme( array( + 'twentytwentythree', 'twentytwentytwo', 'twentytwentyone', 'twentytwenty', diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php index 271e3b9f764..0935b8c85e9 100644 --- a/plugins/woocommerce/includes/wc-template-functions.php +++ b/plugins/woocommerce/includes/wc-template-functions.php @@ -1345,6 +1345,7 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { array_filter( array( 'button', + wc_wp_theme_get_element_class_name( 'button' ), // escaped in the template. 'product_type_' . $product->get_type(), $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '', $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '', @@ -2193,7 +2194,8 @@ if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) * Output the view cart button. */ function woocommerce_widget_shopping_cart_button_view_cart() { - echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; + echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; } } @@ -2203,7 +2205,8 @@ if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) * Output the proceed to checkout button. */ function woocommerce_widget_shopping_cart_proceed_to_checkout() { - echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; + echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; } } @@ -3207,6 +3210,7 @@ if ( ! function_exists( 'woocommerce_account_orders' ) ) { 'current_page' => absint( $current_page ), 'customer_orders' => $customer_orders, 'has_orders' => 0 < $customer_orders->total, + 'wp_button_class' => wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '', ) ); } diff --git a/plugins/woocommerce/templates/auth/form-login.php b/plugins/woocommerce/templates/auth/form-login.php index 3606ca0a7a3..b514d99939e 100644 --- a/plugins/woocommerce/templates/auth/form-login.php +++ b/plugins/woocommerce/templates/auth/form-login.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates\Auth - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -46,7 +46,7 @@ do_action( 'woocommerce_auth_page_header' ); ?>

- +

diff --git a/plugins/woocommerce/templates/cart/cart-empty.php b/plugins/woocommerce/templates/cart/cart-empty.php index 0a463e9a200..e9f60c1d61d 100644 --- a/plugins/woocommerce/templates/cart/cart-empty.php +++ b/plugins/woocommerce/templates/cart/cart-empty.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -24,7 +24,7 @@ do_action( 'woocommerce_cart_is_empty' ); if ( wc_get_page_id( 'shop' ) > 0 ) : ?>

- +

- + diff --git a/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php b/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php index d678926e88c..e02f0b8bfd0 100644 --- a/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php +++ b/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php @@ -14,7 +14,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 2.4.0 + * @version 7.0.1 */ if ( ! defined( 'ABSPATH' ) ) { @@ -22,6 +22,6 @@ if ( ! defined( 'ABSPATH' ) ) { } ?> - + diff --git a/plugins/woocommerce/templates/cart/shipping-calculator.php b/plugins/woocommerce/templates/cart/shipping-calculator.php index 09c0c984f01..d30a63695c7 100644 --- a/plugins/woocommerce/templates/cart/shipping-calculator.php +++ b/plugins/woocommerce/templates/cart/shipping-calculator.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 4.0.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -88,7 +88,7 @@ do_action( 'woocommerce_before_shipping_calculator' ); ?>

-

+

diff --git a/plugins/woocommerce/templates/checkout/form-coupon.php b/plugins/woocommerce/templates/checkout/form-coupon.php index 83e5e3cdaab..9b39e8ee43b 100644 --- a/plugins/woocommerce/templates/checkout/form-coupon.php +++ b/plugins/woocommerce/templates/checkout/form-coupon.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.4 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -36,7 +36,7 @@ if ( ! wc_coupons_enabled() ) { // @codingStandardsIgnoreLine.

- +

diff --git a/plugins/woocommerce/templates/checkout/form-pay.php b/plugins/woocommerce/templates/checkout/form-pay.php index e7bdd506ff3..8a44c973509 100644 --- a/plugins/woocommerce/templates/checkout/form-pay.php +++ b/plugins/woocommerce/templates/checkout/form-pay.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 5.2.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -88,7 +88,7 @@ $totals = $order->get_order_item_totals(); // phpcs:ignore WordPress.WP.GlobalVa - ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> + ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> diff --git a/plugins/woocommerce/templates/checkout/payment.php b/plugins/woocommerce/templates/checkout/payment.php index 7da3423e324..bbe92b8032c 100644 --- a/plugins/woocommerce/templates/checkout/payment.php +++ b/plugins/woocommerce/templates/checkout/payment.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.3 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -41,14 +41,14 @@ if ( ! wp_doing_ajax() ) { /* translators: $1 and $2 opening and closing emphasis tags respectively */ printf( esc_html__( 'Since your browser does not support JavaScript, or it is disabled, please ensure you click the %1$sUpdate Totals%2$s button before placing your order. You may be charged more than the amount stated above if you fail to do so.', 'woocommerce' ), '', '' ); ?> -
+
- ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> + ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> diff --git a/plugins/woocommerce/templates/content-widget-price-filter.php b/plugins/woocommerce/templates/content-widget-price-filter.php index 30610410ede..2bb2541e13f 100644 --- a/plugins/woocommerce/templates/content-widget-price-filter.php +++ b/plugins/woocommerce/templates/content-widget-price-filter.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.7.1 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -29,7 +29,7 @@ defined( 'ABSPATH' ) || exit; - + diff --git a/plugins/woocommerce/templates/global/form-login.php b/plugins/woocommerce/templates/global/form-login.php index a6142132d22..767d13ea60a 100644 --- a/plugins/woocommerce/templates/global/form-login.php +++ b/plugins/woocommerce/templates/global/form-login.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.6.0 + * @version 7.1.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -48,7 +48,7 @@ if ( is_user_logged_in() ) { - +

diff --git a/plugins/woocommerce/templates/myaccount/form-add-payment-method.php b/plugins/woocommerce/templates/myaccount/form-add-payment-method.php index 71b859aa4db..ace49da793a 100644 --- a/plugins/woocommerce/templates/myaccount/form-add-payment-method.php +++ b/plugins/woocommerce/templates/myaccount/form-add-payment-method.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 4.3.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -51,7 +51,7 @@ if ( $available_gateways ) : ?>

- +
diff --git a/plugins/woocommerce/templates/myaccount/form-edit-account.php b/plugins/woocommerce/templates/myaccount/form-edit-account.php index eb5dfd180b5..51a2bf9ecee 100644 --- a/plugins/woocommerce/templates/myaccount/form-edit-account.php +++ b/plugins/woocommerce/templates/myaccount/form-edit-account.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -66,7 +66,7 @@ do_action( 'woocommerce_before_edit_account_form' ); ?>

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-edit-address.php b/plugins/woocommerce/templates/myaccount/form-edit-address.php index 6916bef15c7..ee35dd9f163 100644 --- a/plugins/woocommerce/templates/myaccount/form-edit-address.php +++ b/plugins/woocommerce/templates/myaccount/form-edit-address.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.6.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -43,7 +43,7 @@ do_action( 'woocommerce_before_edit_account_address_form' ); ?>

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-login.php b/plugins/woocommerce/templates/myaccount/form-login.php index cdf38190e69..93c8d44db39 100644 --- a/plugins/woocommerce/templates/myaccount/form-login.php +++ b/plugins/woocommerce/templates/myaccount/form-login.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 6.0.0 + * @version 7.0.1 */ if ( ! defined( 'ABSPATH' ) ) { @@ -51,7 +51,7 @@ do_action( 'woocommerce_before_customer_login_form' ); ?> - +

@@ -104,7 +104,7 @@ do_action( 'woocommerce_before_customer_login_form' ); ?>

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-lost-password.php b/plugins/woocommerce/templates/myaccount/form-lost-password.php index 15fc984a745..96aa173a4d3 100644 --- a/plugins/woocommerce/templates/myaccount/form-lost-password.php +++ b/plugins/woocommerce/templates/myaccount/form-lost-password.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.2 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -35,7 +35,7 @@ do_action( 'woocommerce_before_lost_password_form' );

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-reset-password.php b/plugins/woocommerce/templates/myaccount/form-reset-password.php index 3106f6022da..4e49a9f65c8 100644 --- a/plugins/woocommerce/templates/myaccount/form-reset-password.php +++ b/plugins/woocommerce/templates/myaccount/form-reset-password.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.5 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -42,7 +42,7 @@ do_action( 'woocommerce_before_reset_password_form' );

- +

diff --git a/plugins/woocommerce/templates/myaccount/orders.php b/plugins/woocommerce/templates/myaccount/orders.php index 10fcadc509d..8ac301bb7e5 100644 --- a/plugins/woocommerce/templates/myaccount/orders.php +++ b/plugins/woocommerce/templates/myaccount/orders.php @@ -14,7 +14,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.7.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -67,7 +67,7 @@ do_action( 'woocommerce_before_account_orders', $has_orders ); ?> if ( ! empty( $actions ) ) { foreach ( $actions as $key => $action ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - echo '' . esc_html( $action['name'] ) . ''; + echo '' . esc_html( $action['name'] ) . ''; } } ?> diff --git a/plugins/woocommerce/templates/order/form-tracking.php b/plugins/woocommerce/templates/order/form-tracking.php index 2b2ce8de6b3..c18595e1c97 100644 --- a/plugins/woocommerce/templates/order/form-tracking.php +++ b/plugins/woocommerce/templates/order/form-tracking.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 6.5.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -46,7 +46,7 @@ global $post; do_action( 'woocommerce_order_tracking_form' ); ?> -

+

"> - + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/external.php b/plugins/woocommerce/templates/single-product/add-to-cart/external.php index 92039e72f9b..f7063034d01 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/external.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/external.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -22,7 +22,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
- + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php b/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php index 719eb22165f..aa76d3cc267 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 4.8.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -117,7 +117,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?> - + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/simple.php b/plugins/woocommerce/templates/single-product/add-to-cart/simple.php index 494ece1c465..0affa4ffaf0 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/simple.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/simple.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -46,7 +46,7 @@ if ( $product->is_in_stock() ) : ?> do_action( 'woocommerce_after_add_to_cart_quantity' ); ?> - +
diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php b/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php index 03b7aeb02a9..3518a10fcef 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php @@ -4,7 +4,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -28,7 +28,7 @@ global $product; do_action( 'woocommerce_after_add_to_cart_quantity' ); ?> - + diff --git a/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php index fead6b21622..2edb95f6335 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php @@ -15,23 +15,23 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { */ private function get_checkout_url() { - // Get the checkout URL + // Get the checkout URL. $checkout_page_id = wc_get_page_id( 'checkout' ); $checkout_url = ''; - // Check if there is a checkout page + // Check if there is a checkout page. if ( $checkout_page_id ) { - // Get the permalink + // Get the permalink. $checkout_url = get_permalink( $checkout_page_id ); - // Force SSL if needed + // Force SSL if needed. if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) { $checkout_url = str_replace( 'http:', 'https:', $checkout_url ); } - // Allow filtering of checkout URL + // Allow filtering of checkout URL. $checkout_url = apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); } @@ -44,10 +44,10 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * @since 2.5.0 */ public function test_get_checkout_url_regular() { - // Make sure pages exist + // Make sure pages exist. WC_Install::create_pages(); - // Force SSL checkout + // Force SSL checkout. update_option( 'woocommerce_force_ssl_checkout', 'no' ); $this->assertEquals( $this->get_checkout_url(), wc_get_checkout_url() ); @@ -59,10 +59,10 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * @since 2.5.0 */ public function test_get_checkout_url_ssl() { - // Make sure pages exist + // Make sure pages exist. WC_Install::create_pages(); - // Force SSL checkout + // Force SSL checkout. update_option( 'woocommerce_force_ssl_checkout', 'yes' ); $this->assertEquals( $this->get_checkout_url(), wc_get_checkout_url() ); @@ -74,16 +74,16 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * @since 2.3.0 */ public function test_wc_empty_cart() { - // Create dummy product + // Create dummy product. $product = WC_Helper_Product::create_simple_product(); - // Add the product to the cart + // Add the product to the cart. WC()->cart->add_to_cart( $product->get_id(), 1 ); - // Empty the cart + // Empty the cart. wc_empty_cart(); - // Check if the cart is empty + // Check if the cart is empty. $this->assertEquals( 0, WC()->cart->get_cart_contents_count() ); } @@ -137,24 +137,25 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * Test wc_add_to_cart_message */ public function test_wc_add_to_cart_message() { - $product = WC_Helper_Product::create_simple_product(); + $product = WC_Helper_Product::create_simple_product(); + $wp_button_class = esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); $message = wc_add_to_cart_message( array( $product->get_id() => 1 ), false, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( array( $product->get_id() => 3 ), false, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( array( $product->get_id() => 1 ), true, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( array( $product->get_id() => 3 ), true, true ); - $this->assertEquals( 'View cart 3 × “Dummy Product” have been added to your cart.', $message ); + $this->assertEquals( 'View cart 3 × “Dummy Product” have been added to your cart.', $message ); $message = wc_add_to_cart_message( $product->get_id(), false, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( $product->get_id(), true, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); } } From aaef7f7a65856e952fb8abd3e4c350f3c802e623 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Fri, 28 Oct 2022 09:28:32 -0700 Subject: [PATCH 092/149] [HPOS] Allow line breaks in order notes (admin-side) (#35366) Allow line breaks in order notes (admin-side). * Fix code violations --- plugins/woocommerce/changelog/fix-35112 | 4 ++++ .../class-wc-meta-box-order-data.php | 23 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35112 diff --git a/plugins/woocommerce/changelog/fix-35112 b/plugins/woocommerce/changelog/fix-35112 new file mode 100644 index 00000000000..5375bc7a568 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35112 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Allow line breaks in order note again. diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php index 3811b041491..d090cbb0b54 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php @@ -38,6 +38,13 @@ class WC_Meta_Box_Order_Data { */ public static function init_address_fields() { + /** + * Provides an opportunity to modify the list of order billing fields displayed on the admin. + * + * @since 1.4.0 + * + * @param array Billing fields. + */ self::$billing_fields = apply_filters( 'woocommerce_admin_billing_fields', array( @@ -90,6 +97,13 @@ class WC_Meta_Box_Order_Data { ) ); + /** + * Provides an opportunity to modify the list of order shipping fields displayed on the admin. + * + * @since 1.4.0 + * + * @param array Shipping fields. + */ self::$shipping_fields = apply_filters( 'woocommerce_admin_shipping_fields', array( @@ -533,6 +547,13 @@ class WC_Meta_Box_Order_Data { } } + /** + * Allows 3rd parties to alter whether the customer note should be displayed on the admin. + * + * @since 2.1.0 + * + * @param bool TRUE if the note should be displayed. FALSE otherwise. + */ if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' === get_option( 'woocommerce_enable_order_comments', 'yes' ) ) ) : ?>

@@ -669,7 +690,7 @@ class WC_Meta_Box_Order_Data { // Customer note. if ( isset( $_POST['customer_note'] ) ) { - $props['customer_note'] = sanitize_text_field( wp_unslash( $_POST['customer_note'] ) ); + $props['customer_note'] = sanitize_textarea_field( wp_unslash( $_POST['customer_note'] ) ); } // Save order data. From d9f6ecaa870cd44e58305ac36c56c064ebd1e59f Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 28 Oct 2022 09:31:35 -0700 Subject: [PATCH 093/149] Guard against cases where `get_current_screen()` is undefined. (#35371) This may happen if code invokes the `all_plugins` filter during non-admin requests, for example. --- plugins/woocommerce/changelog/fix-35346-features-controller | 5 +++++ .../woocommerce/src/Internal/Features/FeaturesController.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35346-features-controller diff --git a/plugins/woocommerce/changelog/fix-35346-features-controller b/plugins/woocommerce/changelog/fix-35346-features-controller new file mode 100644 index 00000000000..c14d9ae5068 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35346-features-controller @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: The introduction of the FeaturesController is covered by an earlier changelog entry. Since the feature is unreleased, we probably do not need to itemize this small adjustment. + + diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 92292e30ee0..b4572b9936d 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -712,7 +712,7 @@ class FeaturesController { } // phpcs:disable WordPress.Security.NonceVerification - if ( get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { + if ( ! function_exists( 'get_current_screen' ) || get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { return $list; } From 6b6a33282ac3359adc63347ce781a1aa6d7cc85a Mon Sep 17 00:00:00 2001 From: louwie17 Date: Fri, 28 Oct 2022 14:18:10 -0300 Subject: [PATCH 094/149] Add product description title in old product editor (#35154) * Add product description title to classic product editor * Remove bottom margin as its already added by the parent element * Add changelog * Update css and name, also fix some lint errors * Fix spacing --- ...538_update_long_description_copy_and_layout | 4 ++++ .../woocommerce/client/legacy/css/admin.scss | 18 ++++++++++++++++++ .../client/legacy/js/admin/product-editor.js | 15 +++++++++++++++ .../includes/admin/class-wc-admin-assets.php | 13 +++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout create mode 100644 plugins/woocommerce/client/legacy/js/admin/product-editor.js diff --git a/plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout b/plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout new file mode 100644 index 00000000000..f6970dbec40 --- /dev/null +++ b/plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add Product description title in old editor for clarification. diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index bc570ef2d2e..ecac8fccc44 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -7780,3 +7780,21 @@ table.bar_chart { } } } + +#postdivrich.woocommerce-product-description { + margin-top: 20px; + margin-bottom: 0px; + + .wp-editor-tools { + background: none; + padding-top: 0px; + width: 100%; + } + .wp-editor-wrap { + margin: 6px 12px 0; + } + #post-status-info { + margin: 0px 12px 12px; + width: calc( 100% - 24px ); + } +} \ No newline at end of file diff --git a/plugins/woocommerce/client/legacy/js/admin/product-editor.js b/plugins/woocommerce/client/legacy/js/admin/product-editor.js new file mode 100644 index 00000000000..8f2bafef16a --- /dev/null +++ b/plugins/woocommerce/client/legacy/js/admin/product-editor.js @@ -0,0 +1,15 @@ +/* global woocommerce_admin_product_editor */ +jQuery( function ( $ ) { + $( function () { + var editorWrapper = $( '#postdivrich' ); + + if ( editorWrapper.length ) { + editorWrapper.addClass( 'postbox woocommerce-product-description' ); + editorWrapper.prepend( + '

' + ); + } + } ); +} ); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index 29131b2f075..9dee92915e0 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -233,6 +233,19 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params ); } + // Product description. + if ( in_array( $screen_id, array( 'product' ), true ) ) { + wp_enqueue_script( 'wc-admin-product-editor', WC()->plugin_url() . '/assets/js/admin/product-editor' . $suffix . '.js', array( 'jquery' ), $version, false ); + + wp_localize_script( + 'wc-admin-product-editor', + 'woocommerce_admin_product_editor', + array( + 'i18n_description' => esc_js( __( 'Product description', 'woocommerce' ) ), + ) + ); + } + // Meta boxes. if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { wp_enqueue_media(); From 3123f82b7e6badd0eebe4cec5b1ce19a2dadb266 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 11:04:50 -0700 Subject: [PATCH 095/149] Delete changelog files based on PR 35306 (#35391) Delete changelog files for 35306 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/add-tt3-support | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/add-tt3-support diff --git a/plugins/woocommerce/changelog/add-tt3-support b/plugins/woocommerce/changelog/add-tt3-support deleted file mode 100644 index aeeb9d06453..00000000000 --- a/plugins/woocommerce/changelog/add-tt3-support +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Twenty Twenty-Three theme compatibility. From 8740a8a9700094d98e652eafb164b4b648cb5ec2 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Fri, 28 Oct 2022 17:08:13 -0300 Subject: [PATCH 096/149] Update shipping label banner add meta boxes function (#35212) * Add check to see if we are on a shop_order page. * Update deprecated function call * Add changelog * Fix yoda condition --- .../fix-35211_shipping_label_banner_add_meta_boxes_function | 4 ++++ .../woocommerce/src/Internal/Admin/ShippingLabelBanner.php | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function diff --git a/plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function b/plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function new file mode 100644 index 00000000000..a27f7cc1b08 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update ShippingLabelBanner add_meta_box action to only trigger on shop_order pages and remove deprecated function call. diff --git a/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php b/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php index f3b10b9d11f..200dd36c50e 100644 --- a/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php +++ b/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php @@ -46,7 +46,7 @@ class ShippingLabelBanner { } if ( class_exists( Jetpack_Connection_Manager::class ) ) { - $jetpack_connected = ( new Jetpack_Connection_Manager() )->is_active(); + $jetpack_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner(); } if ( class_exists( '\WC_Connect_Loader' ) ) { @@ -82,6 +82,9 @@ class ShippingLabelBanner { * @param \WP_Post $post Current post object. */ public function add_meta_boxes( $post_type, $post ) { + if ( 'shop_order' !== $post_type ) { + return; + } $order = wc_get_order( $post ); if ( $this->should_show_meta_box() ) { add_meta_box( From fa1ecf6e8bc33b0b2e2b68b9bc92dd7414b25751 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:39:54 -0500 Subject: [PATCH 097/149] Delete changelog files based on PR 35371 (#35400) Delete changelog files for 35371 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-35346-features-controller | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35346-features-controller diff --git a/plugins/woocommerce/changelog/fix-35346-features-controller b/plugins/woocommerce/changelog/fix-35346-features-controller deleted file mode 100644 index c14d9ae5068..00000000000 --- a/plugins/woocommerce/changelog/fix-35346-features-controller +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: The introduction of the FeaturesController is covered by an earlier changelog entry. Since the feature is unreleased, we probably do not need to itemize this small adjustment. - - From 2c5d3d2acc38395ca9be17b8c18c0f21ca9542d3 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Mon, 31 Oct 2022 14:26:28 +0530 Subject: [PATCH 098/149] Check order type is set before returning to prevent notice. (#35349) * Check order type is set before returning to prevent notice. * Applied code standards. * Remove type declaration since its not consistent with CPT datastore. * Switch to a yoda condition (satisfy required linting check). Co-authored-by: barryhughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/changelog/fix-order_type | 4 ++++ .../DataStores/Orders/OrdersTableDataStore.php | 2 +- .../src/Internal/Utilities/COTMigrationUtil.php | 4 ++-- .../Orders/OrdersTableDataStoreTests.php | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-order_type diff --git a/plugins/woocommerce/changelog/fix-order_type b/plugins/woocommerce/changelog/fix-order_type new file mode 100644 index 00000000000..ba0073e6737 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-order_type @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Check order type is set before returning to prevent notice. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 826cc0ce5c6..64885983ab5 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -948,7 +948,7 @@ WHERE */ public function get_order_type( $order_id ) { $type = $this->get_orders_type( array( $order_id ) ); - return $type[ $order_id ]; + return $type[ $order_id ] ?? ''; } /** diff --git a/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php b/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php index 1c88e5074b5..d0a17ffeb47 100644 --- a/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php @@ -75,7 +75,7 @@ class COTMigrationUtil { */ public function is_custom_order_tables_in_sync() : bool { $sync_status = $this->data_synchronizer->get_sync_status(); - return $sync_status['current_pending_count'] === 0 && $this->data_synchronizer->data_sync_is_enabled(); + return 0 === $sync_status['current_pending_count'] && $this->data_synchronizer->data_sync_is_enabled(); } /** @@ -160,7 +160,7 @@ class COTMigrationUtil { * * @return string|null Type of the order. */ - public function get_order_type( $order_id ) : ?string { + public function get_order_type( $order_id ) { $order_id = $this->get_post_or_order_id( $order_id ); $order_data_store = \WC_Data_Store::load( 'order' ); return $order_data_store->get_order_type( $order_id ); diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 37f4f850469..3b512c14328 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -1868,6 +1868,23 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->assertFalse( wc_get_order( $product->get_id() ) ); } + /** + * @testDox Make sure that getting order type for non order return without warning. + */ + public function test_get_order_type_for_non_order() { + $product = WC_Helper_Product::create_simple_product(); + $product->save(); + $this->assertEquals( '', $this->sut->get_order_type( $product->get_id() ) ); + } + + /** + * @testDox Test get order type working as expected. + */ + public function test_get_order_type_for_order() { + $order = $this->create_complex_cot_order(); + $this->assertEquals( 'shop_order', $this->sut->get_order_type( $order->get_id() ) ); + } + /** * @testDox Test that we are not duplicating address indexing when updating. */ From 6c551b0cd646512a60930a9bad8fb02bd55e49b8 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Mon, 31 Oct 2022 14:33:18 +0530 Subject: [PATCH 099/149] Also support syncing for HPOS with stats table. (#35118) * Also support syncing for HPOS with stats table. * Add changelog. * Fixup. * More fixup. * test commit by disabling importing admin test orders. * Update get_items to work with HPOS. * Modify tests to assert against invalid result. * test commit for ci. * Remove seperate test as its quite slow. * Applied coding standards. * Coding standards, part 2. --- plugins/woocommerce/changelog/fix-35032 | 4 + .../Admin/API/Reports/Customers/DataStore.php | 5 +- .../API/Reports/Orders/Stats/DataStore.php | 13 ++- .../Admin/Schedulers/OrdersScheduler.php | 93 ++++++++++++++++++- .../Internal/Utilities/COTMigrationUtil.php | 6 +- .../api/reports-customers.php | 7 +- 6 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-35032 diff --git a/plugins/woocommerce/changelog/fix-35032 b/plugins/woocommerce/changelog/fix-35032 new file mode 100644 index 00000000000..eec41196f73 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35032 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Sync orders for stats table. diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php index 424ab32be94..16b546f4966 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php @@ -12,6 +12,7 @@ use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; +use Automattic\WooCommerce\Utilities\OrderUtil; /** * Admin\API\Reports\Customers\DataStore. @@ -64,7 +65,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { 'id' => "{$table_name}.customer_id as id", 'user_id' => 'user_id', 'username' => 'username', - 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL? + 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @xxx: What does this mean for RTL? 'email' => 'email', 'country' => 'country', 'city' => 'city', @@ -122,7 +123,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { public static function sync_order_customer( $post_id ) { global $wpdb; - if ( 'shop_order' !== get_post_type( $post_id ) && 'shop_order_refund' !== get_post_type( $post_id ) ) { + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { return -1; } diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php index 232320d2f08..b4661107704 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php @@ -13,6 +13,7 @@ use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; +use Automattic\WooCommerce\Utilities\OrderUtil; /** * API\Reports\Orders\Stats\DataStore. @@ -113,6 +114,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * @param array $query_args Query arguments supplied by the user. */ protected function orders_stats_sql_filter( $query_args ) { + // phpcs:ignore Generic.Commenting.Todo.TaskFound // @todo Performance of all of this? global $wpdb; @@ -335,6 +337,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) ); } + // phpcs:ignore Generic.Commenting.Todo.TaskFound // @todo Remove these assignements when refactoring segmenter classes to use query objects. $totals_query = array( 'from_clause' => $this->total_query->get_sql_clause( 'join' ), @@ -474,7 +477,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success. */ public static function sync_order( $post_id ) { - if ( 'shop_order' !== get_post_type( $post_id ) && 'shop_order_refund' !== get_post_type( $post_id ) ) { + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { return -1; } @@ -505,6 +508,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * * @param array $data Data written to order stats lookup table. * @param WC_Order $order Order object. + * + * @since 4.0.0 */ $data = apply_filters( 'woocommerce_analytics_update_order_stats_data', @@ -555,6 +560,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * Fires when order's stats reports are updated. * * @param int $order_id Order ID. + * + * @since 4.0.0. */ do_action( 'woocommerce_analytics_update_order_stats', $order->get_id() ); @@ -571,7 +578,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { global $wpdb; $order_id = (int) $post_id; - if ( 'shop_order' !== get_post_type( $order_id ) && 'shop_order_refund' !== get_post_type( $order_id ) ) { + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { return; } @@ -586,6 +593,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * * @param int $order_id Order ID. * @param int $customer_id Customer ID. + * + * @since 4.0.0 */ do_action( 'woocommerce_analytics_delete_order_stats', $order_id, $customer_id ); diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php index 9e43152d523..f9592146d94 100644 --- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php +++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php @@ -13,6 +13,9 @@ use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsData use \Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore as TaxesDataStore; use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; +use Automattic\WooCommerce\Admin\Overrides\Order; +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; +use Automattic\WooCommerce\Utilities\OrderUtil; /** * OrdersScheduler Class. @@ -36,7 +39,8 @@ class OrdersScheduler extends ImportScheduler { \Automattic\WooCommerce\Admin\Overrides\OrderRefund::add_filters(); // Order and refund data must be run on these hooks to ensure meta data is set. - add_action( 'save_post', array( __CLASS__, 'possibly_schedule_import' ) ); + add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) ); + add_action( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) ); add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) ); OrdersStatsDataStore::init(); @@ -69,6 +73,25 @@ class OrdersScheduler extends ImportScheduler { * @param bool $skip_existing Skip already imported orders. */ public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) { + if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { + return self::get_items_from_orders_table( $limit, $page, $days, $skip_existing ); + } else { + return self::get_items_from_posts_table( $limit, $page, $days, $skip_existing ); + } + } + + /** + * Helper method to ger order/refund IDS and total count that needs to be synced. + * + * @internal + * @param int $limit Number of records to retrieve. + * @param int $page Page number. + * @param int|bool $days Number of days prior to current date to limit search results. + * @param bool $skip_existing Skip already imported orders. + * + * @return object Total counts. + */ + private static function get_items_from_posts_table( $limit, $page, $days, $skip_existing ) { global $wpdb; $where_clause = ''; $offset = $page > 1 ? ( $page - 1 ) * $limit : 0; @@ -112,6 +135,65 @@ class OrdersScheduler extends ImportScheduler { ); } + /** + * Helper method to ger order/refund IDS and total count that needs to be synced from HPOS. + * + * @internal + * @param int $limit Number of records to retrieve. + * @param int $page Page number. + * @param int|bool $days Number of days prior to current date to limit search results. + * @param bool $skip_existing Skip already imported orders. + * + * @return object Total counts. + */ + private static function get_items_from_orders_table( $limit, $page, $days, $skip_existing ) { + global $wpdb; + $where_clause = ''; + $offset = $page > 1 ? ( $page - 1 ) * $limit : 0; + $order_table = OrdersTableDataStore::get_orders_table_name(); + + if ( is_int( $days ) ) { + $days_ago = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ); + $where_clause .= " AND orders.date_created_gmt >= '{$days_ago}'"; + } + + if ( $skip_existing ) { + $where_clause .= "AND NOT EXiSTS ( + SELECT 1 FROM {$wpdb->prefix}wc_order_stats + WHERE {$wpdb->prefix}wc_order_stats.order_id = orders.id + ) + "; + } + + $count = $wpdb->get_var( + " +SELECT COUNT(*) FROM {$order_table} AS orders +WHERE type in ( 'shop_order', 'shop_order_refund' ) +AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' ) +{$where_clause} +" + ); // phpcs:ignore unprepared SQL ok. + + $order_ids = absint( $count ) > 0 ? $wpdb->get_col( + $wpdb->prepare( + "SELECT id FROM {$order_table} AS orders + WHERE type IN ( 'shop_order', 'shop_order_refund' ) + AND status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' ) + {$where_clause} + ORDER BY date_created_gmt ASC + LIMIT %d + OFFSET %d", + $limit, + $offset + ) + ) : array(); // phpcs:ignore unprepared SQL ok. + + return (object) array( + 'total' => absint( $count ), + 'ids' => $order_ids, + ); + } + /** * Get total number of rows imported. * @@ -125,15 +207,16 @@ class OrdersScheduler extends ImportScheduler { /** * Schedule this import if the post is an order or refund. * + * @param int $order_id Post ID. + * * @internal - * @param int $post_id Post ID. */ - public static function possibly_schedule_import( $post_id ) { - if ( 'shop_order' !== get_post_type( $post_id ) && 'woocommerce_refund_created' !== current_filter() ) { + public static function possibly_schedule_import( $order_id ) { + if ( ! OrderUtil::is_order( $order_id, array( 'shop_order' ) ) && 'woocommerce_refund_created' !== current_filter() ) { return; } - self::schedule_action( 'import', array( $post_id ) ); + self::schedule_action( 'import', array( $order_id ) ); } /** diff --git a/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php b/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php index d0a17ffeb47..1f0d52cc547 100644 --- a/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php @@ -122,15 +122,15 @@ class COTMigrationUtil { } /** - * Helper function to id from an post or order object. + * Helper function to get ID from a post or order object. * * @param WP_Post/WC_Order $post_or_order_object WP_Post/WC_Order object to get ID for. * * @return int Order or post ID. */ public function get_post_or_order_id( $post_or_order_object ) : int { - if ( is_int( $post_or_order_object ) ) { - return $post_or_order_object; + if ( is_numeric( $post_or_order_object ) ) { + return (int) $post_or_order_object; } elseif ( $post_or_order_object instanceof WC_Order ) { return $post_or_order_object->get_id(); } elseif ( $post_or_order_object instanceof WP_Post ) { diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php index 3ea07b24fc8..75d7d0ca8dc 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php @@ -6,6 +6,8 @@ * @since 3.5.0 */ +// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps + use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; /** @@ -503,7 +505,8 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { WC_Helper_Queue::run_all_pending(); - $this->assertTrue( $result ); + $this->assertNotEquals( -1, $result ); + $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); $reports = $response->get_data(); @@ -599,7 +602,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { WC_Helper_Queue::run_all_pending(); // Didn't update anything. - $this->assertTrue( $result ); + $this->assertNotEquals( -1, $result ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); $reports = $response->get_data(); From 11e22063caedc74459043e955bfb4368ee05ce6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Mon, 31 Oct 2022 12:41:06 +0100 Subject: [PATCH 100/149] Exclude inactive plugins from the incompatible plugins view (#35333) * Changes in the plugin-feature compatibility warnings: - Show only active plugins in the "Incompatible with..." plugins view - Show an empty page when the view is loaded and no Incompatible plugins are active (instead of listing all the existing plugins) * Add changelog file --- ...compatibility_warnings_for_inactive_plugins | 4 ++++ .../Internal/Features/FeaturesController.php | 18 +++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins diff --git a/plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins b/plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins new file mode 100644 index 00000000000..2731dfabc35 --- /dev/null +++ b/plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Don't show feature compatibility warnings for inactive plugins diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index b4572b9936d..bf2622df5c2 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -725,7 +725,7 @@ class FeaturesController { // phpcs:enable WordPress.Security.NonceVerification foreach ( array_keys( $list ) as $plugin_name ) { - if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) ) { + if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) || ! $this->proxy->call_function( 'is_plugin_active', $plugin_name ) ) { continue; } @@ -740,10 +740,6 @@ class FeaturesController { } } - if ( 0 === count( $incompatibles ) ) { - return $list; - } - return array_intersect_key( $list, array_flip( $incompatibles ) ); } @@ -832,10 +828,10 @@ class FeaturesController { $message = 'all' === $feature_id - ? __( 'You are viewing plugins that are incompatible with currently enabled WooCommerce features.', 'woocommerce' ) + ? __( 'You are viewing active plugins that are incompatible with currently enabled WooCommerce features.', 'woocommerce' ) : sprintf( /* translators: %s is a feature name. */ - __( "You are viewing the plugins that are incompatible with the '%s' feature.", 'woocommerce' ), + __( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ), $this->features[ $feature_id ]['name'] ); @@ -867,11 +863,15 @@ class FeaturesController { private function handle_plugin_list_rows( $plugin_file, $plugin_data ) { global $wp_list_table; + if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return; + } + if ( is_null( $wp_list_table ) || ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_data ) ) { return; } - if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { // phpcs:ignore WordPress.Security.NonceVerification + if ( ! $this->proxy->call_function( 'is_plugin_active', $plugin_file ) ) { return; } @@ -889,7 +889,7 @@ class FeaturesController { $incompatible_features_count = count( $incompatible_features ); if ( $incompatible_features_count > 0 ) { $columns_count = $wp_list_table->get_column_count(); - $is_active = $this->proxy->call_function( 'is_plugin_active', $plugin_file ); + $is_active = true; // For now we are showing active plugins in the "Incompatible with..." view. $is_active_class = $is_active ? 'active' : 'inactive'; $is_active_td_style = $is_active ? " style='border-left: 4px solid #72aee6;'" : ''; From 19f0410bc1919bc909879c886b3a3eef2c739d8a Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 1 Nov 2022 07:53:01 +1300 Subject: [PATCH 101/149] PHPCS: Lint only changes (#35407) Add `sirbrillig/phpcs-changed` to run PHPCS only on changes, not entire files --- .../setup-woocommerce-monorepo/action.yml | 2 +- .github/workflows/pr-code-sniff.yml | 75 ++++++++++--------- .../changelog/try-add-phpcs-changed | 5 ++ .../admin/class-wc-admin-exporters.php | 2 +- 4 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 plugins/woocommerce/changelog/try-add-phpcs-changed diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index 3ca1431d678..b31441bce31 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -42,7 +42,7 @@ runs: with: php-version: ${{ inputs.php-version }} coverage: none - tools: cs2pr, phpcs + tools: phpcs, sirbrillig/phpcs-changed - name: Cache Composer Dependencies uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml index 7a8254850d8..2eee0a02a3a 100644 --- a/.github/workflows/pr-code-sniff.yml +++ b/.github/workflows/pr-code-sniff.yml @@ -1,41 +1,46 @@ name: Run code sniff on PR -on: - pull_request +on: pull_request defaults: - run: - shell: bash -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + run: + shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +env: + PHPCS: ./plugins/woocommerce/vendor/bin/phpcs # Run WooCommerce phpcs setup in phpcs-changed instead of default jobs: - test: - name: Code sniff (PHP 7.4, WP Latest) - timeout-minutes: 15 - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Get Changed Files - id: changed-files - uses: tj-actions/changed-files@v32 - with: - files: | - **/*.php + test: + name: Code sniff (PHP 7.4, WP Latest) + timeout-minutes: 15 + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Setup WooCommerce Monorepo - if: steps.changed-files.outputs.any_changed == 'true' - uses: ./.github/actions/setup-woocommerce-monorepo - with: - build: false + - name: Get Changed Files + id: changed-files + uses: tj-actions/changed-files@v32 + with: + files: | + **/*.php - - name: Tool versions - if: steps.changed-files.outputs.any_changed == 'true' - run: | - php --version - composer --version + - name: Setup WooCommerce Monorepo + if: steps.changed-files.outputs.any_changed == 'true' + uses: ./.github/actions/setup-woocommerce-monorepo + with: + build: false - - name: Run PHPCS - if: steps.changed-files.outputs.any_changed == 'true' - run: ./plugins/woocommerce/vendor/bin/phpcs -n -q --report=checkstyle ${{ steps.changed-files.outputs.all_changed_files }} | cs2pr + - name: Tool versions + if: steps.changed-files.outputs.any_changed == 'true' + run: | + php --version + composer --version + phpcs-changed --version + + - name: Run PHPCS + if: steps.changed-files.outputs.any_changed == 'true' + run: | + HEAD_REF=$(git rev-parse HEAD) + git checkout $HEAD_REF + phpcs-changed --git --git-base ${{ github.base_ref }} ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/plugins/woocommerce/changelog/try-add-phpcs-changed b/plugins/woocommerce/changelog/try-add-phpcs-changed new file mode 100644 index 00000000000..a235f670cdf --- /dev/null +++ b/plugins/woocommerce/changelog/try-add-phpcs-changed @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Just whitespace change, no entry required + + diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php b/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php index 9a0758ff26d..07b4526bc9f 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php @@ -199,7 +199,7 @@ class WC_Admin_Exporters { * @return array The product types keys and labels. */ public static function get_product_types() { - $product_types = wc_get_product_types(); + $product_types = wc_get_product_types(); $product_types['variation'] = __( 'Product variations', 'woocommerce' ); /** From edb59aef4defb03d428d5ac7a56914f410ee8578 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Mon, 31 Oct 2022 14:22:15 -0500 Subject: [PATCH 102/149] Update/blocks 8.7.5 (#35428) * Update blocks to 8.7.5 * Add changelog file --- plugins/woocommerce/changelog/update-blocks-8.7.5 | 4 ++++ plugins/woocommerce/composer.json | 2 +- plugins/woocommerce/composer.lock | 14 +++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-blocks-8.7.5 diff --git a/plugins/woocommerce/changelog/update-blocks-8.7.5 b/plugins/woocommerce/changelog/update-blocks-8.7.5 new file mode 100644 index 00000000000..e102e49b550 --- /dev/null +++ b/plugins/woocommerce/changelog/update-blocks-8.7.5 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update WooCommerce Blocks to 8.7.5 diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 1da4b8966b2..f2a9ca4ef70 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.4.2", - "woocommerce/woocommerce-blocks": "8.7.4" + "woocommerce/woocommerce-blocks": "8.7.5" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index d5d420dea28..809b2e3c2e7 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -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": "9d9a10e340f2c9d2366a082eb3b91344", + "content-hash": "08d58a387b373d546fb53025a230e768", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v8.7.4", + "version": "v8.7.5", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "8553c41d5141725f2e399dab8527b573492402ea" + "reference": "0436c8afb8c3c34dd38aed2b7a0868e771036031" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/8553c41d5141725f2e399dab8527b573492402ea", - "reference": "8553c41d5141725f2e399dab8527b573492402ea", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/0436c8afb8c3c34dd38aed2b7a0868e771036031", + "reference": "0436c8afb8c3c34dd38aed2b7a0868e771036031", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.4" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.5" }, - "time": "2022-10-21T15:55:49+00:00" + "time": "2022-10-31T14:54:55+00:00" } ], "packages-dev": [ From 613be5a990f1becb0553d5737b640274b45005ae Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Mon, 31 Oct 2022 19:23:10 +0000 Subject: [PATCH 103/149] add/a2p data api-core-tests (#35347) * add data api-core-tests * add newline to see if this resolves changelog issue * add newline to see if this resolves changelog issue --- .../add-api-core-tests-data-crud-tests | 4 + .../tests/data/data-crud.test.js | 27365 ++++++++++++++++ 2 files changed, 27369 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests create mode 100644 plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js diff --git a/plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests new file mode 100644 index 00000000000..d2bec88cc88 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for data crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js new file mode 100644 index 00000000000..bcf5d752d9d --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js @@ -0,0 +1,27365 @@ +const { + test, + expect +} = require('@playwright/test'); +const exp = require('constants'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group data + * + */ +test.describe('Data API tests', () => { + + test('can list all data', async ({ + request + }) => { + // call API to retrieve data values + const response = await request.get('/wp-json/wc/v3/data'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "continents", + "description": "List of supported continents, countries, and states.", + }), + + expect.objectContaining({ + "slug": "countries", + "description": "List of supported states in a given country.", + }), + + expect.objectContaining({ + "slug": "currencies", + "description": "List of supported currencies.", + }), + + ]) + ); + }); + + test('can view all continents', async ({ + request + }) => { + // call API to retrieve all continents + const response = await request.get('/wp-json/wc/v3/data/continents'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "AF", + "name": "Africa", + "countries": [{ + "code": "AO", + "name": "Angolan kwanza", + "currency_code": "AOA", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BGO", + "name": "Bengo" + }, + { + "code": "BLU", + "name": "Benguela" + }, + { + "code": "BIE", + "name": "Bié" + }, + { + "code": "CAB", + "name": "Cabinda" + }, + { + "code": "CNN", + "name": "Cunene" + }, + { + "code": "HUA", + "name": "Huambo" + }, + { + "code": "HUI", + "name": "Huíla" + }, + { + "code": "CCU", + "name": "Kuando Kubango" + }, + { + "code": "CNO", + "name": "Kwanza-Norte" + }, + { + "code": "CUS", + "name": "Kwanza-Sul" + }, + { + "code": "LUA", + "name": "Luanda" + }, + { + "code": "LNO", + "name": "Lunda-Norte" + }, + { + "code": "LSU", + "name": "Lunda-Sul" + }, + { + "code": "MAL", + "name": "Malanje" + }, + { + "code": "MOX", + "name": "Moxico" + }, + { + "code": "NAM", + "name": "Namibe" + }, + { + "code": "UIG", + "name": "Uíge" + }, + { + "code": "ZAI", + "name": "Zaire" + } + ] + }, + { + "code": "BF", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BI", + "name": "Burundian franc", + "currency_code": "BIF", + "currency_pos": "right", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BJ", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "AL", + "name": "Alibori" + }, + { + "code": "AK", + "name": "Atakora" + }, + { + "code": "AQ", + "name": "Atlantique" + }, + { + "code": "BO", + "name": "Borgou" + }, + { + "code": "CO", + "name": "Collines" + }, + { + "code": "KO", + "name": "Kouffo" + }, + { + "code": "DO", + "name": "Donga" + }, + { + "code": "LI", + "name": "Littoral" + }, + { + "code": "MO", + "name": "Mono" + }, + { + "code": "OU", + "name": "Ouémé" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "ZO", + "name": "Zou" + } + ] + }, + { + "code": "BW", + "name": "Botswana pula", + "currency_code": "BWP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CD", + "name": "Congolese franc", + "currency_code": "CDF", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CF", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CG", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CI", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CM", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CV", + "name": "Cape Verdean escudo", + "currency_code": "CVE", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DJ", + "name": "Djiboutian franc", + "currency_code": "DJF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DZ", + "name": "Algerian dinar", + "currency_code": "DZD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "DZ-01", + "name": "Adrar" + }, + { + "code": "DZ-02", + "name": "Chlef" + }, + { + "code": "DZ-03", + "name": "Laghouat" + }, + { + "code": "DZ-04", + "name": "Oum El Bouaghi" + }, + { + "code": "DZ-05", + "name": "Batna" + }, + { + "code": "DZ-06", + "name": "Béjaïa" + }, + { + "code": "DZ-07", + "name": "Biskra" + }, + { + "code": "DZ-08", + "name": "Béchar" + }, + { + "code": "DZ-09", + "name": "Blida" + }, + { + "code": "DZ-10", + "name": "Bouira" + }, + { + "code": "DZ-11", + "name": "Tamanghasset" + }, + { + "code": "DZ-12", + "name": "Tébessa" + }, + { + "code": "DZ-13", + "name": "Tlemcen" + }, + { + "code": "DZ-14", + "name": "Tiaret" + }, + { + "code": "DZ-15", + "name": "Tizi Ouzou" + }, + { + "code": "DZ-16", + "name": "Algiers" + }, + { + "code": "DZ-17", + "name": "Djelfa" + }, + { + "code": "DZ-18", + "name": "Jijel" + }, + { + "code": "DZ-19", + "name": "Sétif" + }, + { + "code": "DZ-20", + "name": "Saïda" + }, + { + "code": "DZ-21", + "name": "Skikda" + }, + { + "code": "DZ-22", + "name": "Sidi Bel Abbès" + }, + { + "code": "DZ-23", + "name": "Annaba" + }, + { + "code": "DZ-24", + "name": "Guelma" + }, + { + "code": "DZ-25", + "name": "Constantine" + }, + { + "code": "DZ-26", + "name": "Médéa" + }, + { + "code": "DZ-27", + "name": "Mostaganem" + }, + { + "code": "DZ-28", + "name": "M’Sila" + }, + { + "code": "DZ-29", + "name": "Mascara" + }, + { + "code": "DZ-30", + "name": "Ouargla" + }, + { + "code": "DZ-31", + "name": "Oran" + }, + { + "code": "DZ-32", + "name": "El Bayadh" + }, + { + "code": "DZ-33", + "name": "Illizi" + }, + { + "code": "DZ-34", + "name": "Bordj Bou Arréridj" + }, + { + "code": "DZ-35", + "name": "Boumerdès" + }, + { + "code": "DZ-36", + "name": "El Tarf" + }, + { + "code": "DZ-37", + "name": "Tindouf" + }, + { + "code": "DZ-38", + "name": "Tissemsilt" + }, + { + "code": "DZ-39", + "name": "El Oued" + }, + { + "code": "DZ-40", + "name": "Khenchela" + }, + { + "code": "DZ-41", + "name": "Souk Ahras" + }, + { + "code": "DZ-42", + "name": "Tipasa" + }, + { + "code": "DZ-43", + "name": "Mila" + }, + { + "code": "DZ-44", + "name": "Aïn Defla" + }, + { + "code": "DZ-45", + "name": "Naama" + }, + { + "code": "DZ-46", + "name": "Aïn Témouchent" + }, + { + "code": "DZ-47", + "name": "Ghardaïa" + }, + { + "code": "DZ-48", + "name": "Relizane" + } + ] + }, + { + "code": "EG", + "name": "Egyptian pound", + "currency_code": "EGP", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "EGALX", + "name": "Alexandria" + }, + { + "code": "EGASN", + "name": "Aswan" + }, + { + "code": "EGAST", + "name": "Asyut" + }, + { + "code": "EGBA", + "name": "Red Sea" + }, + { + "code": "EGBH", + "name": "Beheira" + }, + { + "code": "EGBNS", + "name": "Beni Suef" + }, + { + "code": "EGC", + "name": "Cairo" + }, + { + "code": "EGDK", + "name": "Dakahlia" + }, + { + "code": "EGDT", + "name": "Damietta" + }, + { + "code": "EGFYM", + "name": "Faiyum" + }, + { + "code": "EGGH", + "name": "Gharbia" + }, + { + "code": "EGGZ", + "name": "Giza" + }, + { + "code": "EGIS", + "name": "Ismailia" + }, + { + "code": "EGJS", + "name": "South Sinai" + }, + { + "code": "EGKB", + "name": "Qalyubia" + }, + { + "code": "EGKFS", + "name": "Kafr el-Sheikh" + }, + { + "code": "EGKN", + "name": "Qena" + }, + { + "code": "EGLX", + "name": "Luxor" + }, + { + "code": "EGMN", + "name": "Minya" + }, + { + "code": "EGMNF", + "name": "Monufia" + }, + { + "code": "EGMT", + "name": "Matrouh" + }, + { + "code": "EGPTS", + "name": "Port Said" + }, + { + "code": "EGSHG", + "name": "Sohag" + }, + { + "code": "EGSHR", + "name": "Al Sharqia" + }, + { + "code": "EGSIN", + "name": "North Sinai" + }, + { + "code": "EGSUZ", + "name": "Suez" + }, + { + "code": "EGWAD", + "name": "New Valley" + } + ] + }, + { + "code": "EH", + "name": "Moroccan dirham", + "currency_code": "MAD", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ER", + "name": "Eritrean nakfa", + "currency_code": "ERN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ET", + "name": "Ethiopian birr", + "currency_code": "ETB", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GA", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GH", + "name": "Ghana cedi", + "currency_code": "GHS", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AF", + "name": "Ahafo" + }, + { + "code": "AH", + "name": "Ashanti" + }, + { + "code": "BA", + "name": "Brong-Ahafo" + }, + { + "code": "BO", + "name": "Bono" + }, + { + "code": "BE", + "name": "Bono East" + }, + { + "code": "CP", + "name": "Central" + }, + { + "code": "EP", + "name": "Eastern" + }, + { + "code": "AA", + "name": "Greater Accra" + }, + { + "code": "NE", + "name": "North East" + }, + { + "code": "NP", + "name": "Northern" + }, + { + "code": "OT", + "name": "Oti" + }, + { + "code": "SV", + "name": "Savannah" + }, + { + "code": "UE", + "name": "Upper East" + }, + { + "code": "UW", + "name": "Upper West" + }, + { + "code": "TV", + "name": "Volta" + }, + { + "code": "WP", + "name": "Western" + }, + { + "code": "WN", + "name": "Western North" + } + ] + }, + { + "code": "GM", + "name": "Gambian dalasi", + "currency_code": "GMD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GN", + "name": "Guinean franc", + "currency_code": "GNF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GQ", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GW", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KE", + "name": "Kenyan shilling", + "currency_code": "KES", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "KE01", + "name": "Baringo" + }, + { + "code": "KE02", + "name": "Bomet" + }, + { + "code": "KE03", + "name": "Bungoma" + }, + { + "code": "KE04", + "name": "Busia" + }, + { + "code": "KE05", + "name": "Elgeyo-Marakwet" + }, + { + "code": "KE06", + "name": "Embu" + }, + { + "code": "KE07", + "name": "Garissa" + }, + { + "code": "KE08", + "name": "Homa Bay" + }, + { + "code": "KE09", + "name": "Isiolo" + }, + { + "code": "KE10", + "name": "Kajiado" + }, + { + "code": "KE11", + "name": "Kakamega" + }, + { + "code": "KE12", + "name": "Kericho" + }, + { + "code": "KE13", + "name": "Kiambu" + }, + { + "code": "KE14", + "name": "Kilifi" + }, + { + "code": "KE15", + "name": "Kirinyaga" + }, + { + "code": "KE16", + "name": "Kisii" + }, + { + "code": "KE17", + "name": "Kisumu" + }, + { + "code": "KE18", + "name": "Kitui" + }, + { + "code": "KE19", + "name": "Kwale" + }, + { + "code": "KE20", + "name": "Laikipia" + }, + { + "code": "KE21", + "name": "Lamu" + }, + { + "code": "KE22", + "name": "Machakos" + }, + { + "code": "KE23", + "name": "Makueni" + }, + { + "code": "KE24", + "name": "Mandera" + }, + { + "code": "KE25", + "name": "Marsabit" + }, + { + "code": "KE26", + "name": "Meru" + }, + { + "code": "KE27", + "name": "Migori" + }, + { + "code": "KE28", + "name": "Mombasa" + }, + { + "code": "KE29", + "name": "Murang’a" + }, + { + "code": "KE30", + "name": "Nairobi County" + }, + { + "code": "KE31", + "name": "Nakuru" + }, + { + "code": "KE32", + "name": "Nandi" + }, + { + "code": "KE33", + "name": "Narok" + }, + { + "code": "KE34", + "name": "Nyamira" + }, + { + "code": "KE35", + "name": "Nyandarua" + }, + { + "code": "KE36", + "name": "Nyeri" + }, + { + "code": "KE37", + "name": "Samburu" + }, + { + "code": "KE38", + "name": "Siaya" + }, + { + "code": "KE39", + "name": "Taita-Taveta" + }, + { + "code": "KE40", + "name": "Tana River" + }, + { + "code": "KE41", + "name": "Tharaka-Nithi" + }, + { + "code": "KE42", + "name": "Trans Nzoia" + }, + { + "code": "KE43", + "name": "Turkana" + }, + { + "code": "KE44", + "name": "Uasin Gishu" + }, + { + "code": "KE45", + "name": "Vihiga" + }, + { + "code": "KE46", + "name": "Wajir" + }, + { + "code": "KE47", + "name": "West Pokot" + } + ] + }, + { + "code": "KM", + "name": "Comorian franc", + "currency_code": "KMF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LR", + "name": "Liberian dollar", + "currency_code": "LRD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "BM", + "name": "Bomi" + }, + { + "code": "BN", + "name": "Bong" + }, + { + "code": "GA", + "name": "Gbarpolu" + }, + { + "code": "GB", + "name": "Grand Bassa" + }, + { + "code": "GC", + "name": "Grand Cape Mount" + }, + { + "code": "GG", + "name": "Grand Gedeh" + }, + { + "code": "GK", + "name": "Grand Kru" + }, + { + "code": "LO", + "name": "Lofa" + }, + { + "code": "MA", + "name": "Margibi" + }, + { + "code": "MY", + "name": "Maryland" + }, + { + "code": "MO", + "name": "Montserrado" + }, + { + "code": "NM", + "name": "Nimba" + }, + { + "code": "RV", + "name": "Rivercess" + }, + { + "code": "RG", + "name": "River Gee" + }, + { + "code": "SN", + "name": "Sinoe" + } + ] + }, + { + "code": "LS", + "name": "Lesotho loti", + "currency_code": "LSL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LY", + "name": "Libyan dinar", + "currency_code": "LYD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MA", + "name": "Moroccan dirham", + "currency_code": "MAD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MG", + "name": "Malagasy ariary", + "currency_code": "MGA", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ML", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MR", + "name": "Mauritanian ouguiya", + "currency_code": "MRU", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MU", + "name": "Mauritian rupee", + "currency_code": "MUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MW", + "name": "Malawian kwacha", + "currency_code": "MWK", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MZ", + "name": "Mozambican metical", + "currency_code": "MZN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "MZP", + "name": "Cabo Delgado" + }, + { + "code": "MZG", + "name": "Gaza" + }, + { + "code": "MZI", + "name": "Inhambane" + }, + { + "code": "MZB", + "name": "Manica" + }, + { + "code": "MZL", + "name": "Maputo Province" + }, + { + "code": "MZMPM", + "name": "Maputo" + }, + { + "code": "MZN", + "name": "Nampula" + }, + { + "code": "MZA", + "name": "Niassa" + }, + { + "code": "MZS", + "name": "Sofala" + }, + { + "code": "MZT", + "name": "Tete" + }, + { + "code": "MZQ", + "name": "Zambézia" + } + ] + }, + { + "code": "NA", + "name": "Namibian dollar", + "currency_code": "NAD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ER", + "name": "Erongo" + }, + { + "code": "HA", + "name": "Hardap" + }, + { + "code": "KA", + "name": "Karas" + }, + { + "code": "KE", + "name": "Kavango East" + }, + { + "code": "KW", + "name": "Kavango West" + }, + { + "code": "KH", + "name": "Khomas" + }, + { + "code": "KU", + "name": "Kunene" + }, + { + "code": "OW", + "name": "Ohangwena" + }, + { + "code": "OH", + "name": "Omaheke" + }, + { + "code": "OS", + "name": "Omusati" + }, + { + "code": "ON", + "name": "Oshana" + }, + { + "code": "OT", + "name": "Oshikoto" + }, + { + "code": "OD", + "name": "Otjozondjupa" + }, + { + "code": "CA", + "name": "Zambezi" + } + ] + }, + { + "code": "NE", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NG", + "name": "Nigerian naira", + "currency_code": "NGN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Abia" + }, + { + "code": "FC", + "name": "Abuja" + }, + { + "code": "AD", + "name": "Adamawa" + }, + { + "code": "AK", + "name": "Akwa Ibom" + }, + { + "code": "AN", + "name": "Anambra" + }, + { + "code": "BA", + "name": "Bauchi" + }, + { + "code": "BY", + "name": "Bayelsa" + }, + { + "code": "BE", + "name": "Benue" + }, + { + "code": "BO", + "name": "Borno" + }, + { + "code": "CR", + "name": "Cross River" + }, + { + "code": "DE", + "name": "Delta" + }, + { + "code": "EB", + "name": "Ebonyi" + }, + { + "code": "ED", + "name": "Edo" + }, + { + "code": "EK", + "name": "Ekiti" + }, + { + "code": "EN", + "name": "Enugu" + }, + { + "code": "GO", + "name": "Gombe" + }, + { + "code": "IM", + "name": "Imo" + }, + { + "code": "JI", + "name": "Jigawa" + }, + { + "code": "KD", + "name": "Kaduna" + }, + { + "code": "KN", + "name": "Kano" + }, + { + "code": "KT", + "name": "Katsina" + }, + { + "code": "KE", + "name": "Kebbi" + }, + { + "code": "KO", + "name": "Kogi" + }, + { + "code": "KW", + "name": "Kwara" + }, + { + "code": "LA", + "name": "Lagos" + }, + { + "code": "NA", + "name": "Nasarawa" + }, + { + "code": "NI", + "name": "Niger" + }, + { + "code": "OG", + "name": "Ogun" + }, + { + "code": "ON", + "name": "Ondo" + }, + { + "code": "OS", + "name": "Osun" + }, + { + "code": "OY", + "name": "Oyo" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "RI", + "name": "Rivers" + }, + { + "code": "SO", + "name": "Sokoto" + }, + { + "code": "TA", + "name": "Taraba" + }, + { + "code": "YO", + "name": "Yobe" + }, + { + "code": "ZA", + "name": "Zamfara" + } + ] + }, + { + "code": "RE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "RW", + "name": "Rwandan franc", + "currency_code": "RWF", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SC", + "name": "Seychellois rupee", + "currency_code": "SCR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SD", + "name": "Sudanese pound", + "currency_code": "SDG", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SH", + "name": "Saint Helena pound", + "currency_code": "SHP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SL", + "name": "Sierra Leonean leone", + "currency_code": "SLL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SN", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "SNDB", + "name": "Diourbel" + }, + { + "code": "SNDK", + "name": "Dakar" + }, + { + "code": "SNFK", + "name": "Fatick" + }, + { + "code": "SNKA", + "name": "Kaffrine" + }, + { + "code": "SNKD", + "name": "Kolda" + }, + { + "code": "SNKE", + "name": "Kédougou" + }, + { + "code": "SNKL", + "name": "Kaolack" + }, + { + "code": "SNLG", + "name": "Louga" + }, + { + "code": "SNMT", + "name": "Matam" + }, + { + "code": "SNSE", + "name": "Sédhiou" + }, + { + "code": "SNSL", + "name": "Saint-Louis" + }, + { + "code": "SNTC", + "name": "Tambacounda" + }, + { + "code": "SNTH", + "name": "Thiès" + }, + { + "code": "SNZG", + "name": "Ziguinchor" + } + ] + }, + { + "code": "SO", + "name": "Somali shilling", + "currency_code": "SOS", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SS", + "name": "South Sudanese pound", + "currency_code": "SSP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ST", + "name": "São Tomé and Príncipe dobra", + "currency_code": "STN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SZ", + "name": "Swazi lilangeni", + "currency_code": "SZL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TD", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TG", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TN", + "name": "Tunisian dinar", + "currency_code": "TND", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TZ", + "name": "Tanzanian shilling", + "currency_code": "TZS", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "TZ01", + "name": "Arusha" + }, + { + "code": "TZ02", + "name": "Dar es Salaam" + }, + { + "code": "TZ03", + "name": "Dodoma" + }, + { + "code": "TZ04", + "name": "Iringa" + }, + { + "code": "TZ05", + "name": "Kagera" + }, + { + "code": "TZ06", + "name": "Pemba North" + }, + { + "code": "TZ07", + "name": "Zanzibar North" + }, + { + "code": "TZ08", + "name": "Kigoma" + }, + { + "code": "TZ09", + "name": "Kilimanjaro" + }, + { + "code": "TZ10", + "name": "Pemba South" + }, + { + "code": "TZ11", + "name": "Zanzibar South" + }, + { + "code": "TZ12", + "name": "Lindi" + }, + { + "code": "TZ13", + "name": "Mara" + }, + { + "code": "TZ14", + "name": "Mbeya" + }, + { + "code": "TZ15", + "name": "Zanzibar West" + }, + { + "code": "TZ16", + "name": "Morogoro" + }, + { + "code": "TZ17", + "name": "Mtwara" + }, + { + "code": "TZ18", + "name": "Mwanza" + }, + { + "code": "TZ19", + "name": "Coast" + }, + { + "code": "TZ20", + "name": "Rukwa" + }, + { + "code": "TZ21", + "name": "Ruvuma" + }, + { + "code": "TZ22", + "name": "Shinyanga" + }, + { + "code": "TZ23", + "name": "Singida" + }, + { + "code": "TZ24", + "name": "Tabora" + }, + { + "code": "TZ25", + "name": "Tanga" + }, + { + "code": "TZ26", + "name": "Manyara" + }, + { + "code": "TZ27", + "name": "Geita" + }, + { + "code": "TZ28", + "name": "Katavi" + }, + { + "code": "TZ29", + "name": "Njombe" + }, + { + "code": "TZ30", + "name": "Simiyu" + } + ] + }, + { + "code": "UG", + "name": "Ugandan shilling", + "currency_code": "UGX", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "UG314", + "name": "Abim" + }, + { + "code": "UG301", + "name": "Adjumani" + }, + { + "code": "UG322", + "name": "Agago" + }, + { + "code": "UG323", + "name": "Alebtong" + }, + { + "code": "UG315", + "name": "Amolatar" + }, + { + "code": "UG324", + "name": "Amudat" + }, + { + "code": "UG216", + "name": "Amuria" + }, + { + "code": "UG316", + "name": "Amuru" + }, + { + "code": "UG302", + "name": "Apac" + }, + { + "code": "UG303", + "name": "Arua" + }, + { + "code": "UG217", + "name": "Budaka" + }, + { + "code": "UG218", + "name": "Bududa" + }, + { + "code": "UG201", + "name": "Bugiri" + }, + { + "code": "UG235", + "name": "Bugweri" + }, + { + "code": "UG420", + "name": "Buhweju" + }, + { + "code": "UG117", + "name": "Buikwe" + }, + { + "code": "UG219", + "name": "Bukedea" + }, + { + "code": "UG118", + "name": "Bukomansimbi" + }, + { + "code": "UG220", + "name": "Bukwa" + }, + { + "code": "UG225", + "name": "Bulambuli" + }, + { + "code": "UG416", + "name": "Buliisa" + }, + { + "code": "UG401", + "name": "Bundibugyo" + }, + { + "code": "UG430", + "name": "Bunyangabu" + }, + { + "code": "UG402", + "name": "Bushenyi" + }, + { + "code": "UG202", + "name": "Busia" + }, + { + "code": "UG221", + "name": "Butaleja" + }, + { + "code": "UG119", + "name": "Butambala" + }, + { + "code": "UG233", + "name": "Butebo" + }, + { + "code": "UG120", + "name": "Buvuma" + }, + { + "code": "UG226", + "name": "Buyende" + }, + { + "code": "UG317", + "name": "Dokolo" + }, + { + "code": "UG121", + "name": "Gomba" + }, + { + "code": "UG304", + "name": "Gulu" + }, + { + "code": "UG403", + "name": "Hoima" + }, + { + "code": "UG417", + "name": "Ibanda" + }, + { + "code": "UG203", + "name": "Iganga" + }, + { + "code": "UG418", + "name": "Isingiro" + }, + { + "code": "UG204", + "name": "Jinja" + }, + { + "code": "UG318", + "name": "Kaabong" + }, + { + "code": "UG404", + "name": "Kabale" + }, + { + "code": "UG405", + "name": "Kabarole" + }, + { + "code": "UG213", + "name": "Kaberamaido" + }, + { + "code": "UG427", + "name": "Kagadi" + }, + { + "code": "UG428", + "name": "Kakumiro" + }, + { + "code": "UG101", + "name": "Kalangala" + }, + { + "code": "UG222", + "name": "Kaliro" + }, + { + "code": "UG122", + "name": "Kalungu" + }, + { + "code": "UG102", + "name": "Kampala" + }, + { + "code": "UG205", + "name": "Kamuli" + }, + { + "code": "UG413", + "name": "Kamwenge" + }, + { + "code": "UG414", + "name": "Kanungu" + }, + { + "code": "UG206", + "name": "Kapchorwa" + }, + { + "code": "UG236", + "name": "Kapelebyong" + }, + { + "code": "UG126", + "name": "Kasanda" + }, + { + "code": "UG406", + "name": "Kasese" + }, + { + "code": "UG207", + "name": "Katakwi" + }, + { + "code": "UG112", + "name": "Kayunga" + }, + { + "code": "UG407", + "name": "Kibaale" + }, + { + "code": "UG103", + "name": "Kiboga" + }, + { + "code": "UG227", + "name": "Kibuku" + }, + { + "code": "UG432", + "name": "Kikuube" + }, + { + "code": "UG419", + "name": "Kiruhura" + }, + { + "code": "UG421", + "name": "Kiryandongo" + }, + { + "code": "UG408", + "name": "Kisoro" + }, + { + "code": "UG305", + "name": "Kitgum" + }, + { + "code": "UG319", + "name": "Koboko" + }, + { + "code": "UG325", + "name": "Kole" + }, + { + "code": "UG306", + "name": "Kotido" + }, + { + "code": "UG208", + "name": "Kumi" + }, + { + "code": "UG333", + "name": "Kwania" + }, + { + "code": "UG228", + "name": "Kween" + }, + { + "code": "UG123", + "name": "Kyankwanzi" + }, + { + "code": "UG422", + "name": "Kyegegwa" + }, + { + "code": "UG415", + "name": "Kyenjojo" + }, + { + "code": "UG125", + "name": "Kyotera" + }, + { + "code": "UG326", + "name": "Lamwo" + }, + { + "code": "UG307", + "name": "Lira" + }, + { + "code": "UG229", + "name": "Luuka" + }, + { + "code": "UG104", + "name": "Luwero" + }, + { + "code": "UG124", + "name": "Lwengo" + }, + { + "code": "UG114", + "name": "Lyantonde" + }, + { + "code": "UG223", + "name": "Manafwa" + }, + { + "code": "UG320", + "name": "Maracha" + }, + { + "code": "UG105", + "name": "Masaka" + }, + { + "code": "UG409", + "name": "Masindi" + }, + { + "code": "UG214", + "name": "Mayuge" + }, + { + "code": "UG209", + "name": "Mbale" + }, + { + "code": "UG410", + "name": "Mbarara" + }, + { + "code": "UG423", + "name": "Mitooma" + }, + { + "code": "UG115", + "name": "Mityana" + }, + { + "code": "UG308", + "name": "Moroto" + }, + { + "code": "UG309", + "name": "Moyo" + }, + { + "code": "UG106", + "name": "Mpigi" + }, + { + "code": "UG107", + "name": "Mubende" + }, + { + "code": "UG108", + "name": "Mukono" + }, + { + "code": "UG334", + "name": "Nabilatuk" + }, + { + "code": "UG311", + "name": "Nakapiripirit" + }, + { + "code": "UG116", + "name": "Nakaseke" + }, + { + "code": "UG109", + "name": "Nakasongola" + }, + { + "code": "UG230", + "name": "Namayingo" + }, + { + "code": "UG234", + "name": "Namisindwa" + }, + { + "code": "UG224", + "name": "Namutumba" + }, + { + "code": "UG327", + "name": "Napak" + }, + { + "code": "UG310", + "name": "Nebbi" + }, + { + "code": "UG231", + "name": "Ngora" + }, + { + "code": "UG424", + "name": "Ntoroko" + }, + { + "code": "UG411", + "name": "Ntungamo" + }, + { + "code": "UG328", + "name": "Nwoya" + }, + { + "code": "UG331", + "name": "Omoro" + }, + { + "code": "UG329", + "name": "Otuke" + }, + { + "code": "UG321", + "name": "Oyam" + }, + { + "code": "UG312", + "name": "Pader" + }, + { + "code": "UG332", + "name": "Pakwach" + }, + { + "code": "UG210", + "name": "Pallisa" + }, + { + "code": "UG110", + "name": "Rakai" + }, + { + "code": "UG429", + "name": "Rubanda" + }, + { + "code": "UG425", + "name": "Rubirizi" + }, + { + "code": "UG431", + "name": "Rukiga" + }, + { + "code": "UG412", + "name": "Rukungiri" + }, + { + "code": "UG111", + "name": "Sembabule" + }, + { + "code": "UG232", + "name": "Serere" + }, + { + "code": "UG426", + "name": "Sheema" + }, + { + "code": "UG215", + "name": "Sironko" + }, + { + "code": "UG211", + "name": "Soroti" + }, + { + "code": "UG212", + "name": "Tororo" + }, + { + "code": "UG113", + "name": "Wakiso" + }, + { + "code": "UG313", + "name": "Yumbe" + }, + { + "code": "UG330", + "name": "Zombo" + } + ] + }, + { + "code": "YT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ZA", + "name": "South African rand", + "currency_code": "ZAR", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "EC", + "name": "Eastern Cape" + }, + { + "code": "FS", + "name": "Free State" + }, + { + "code": "GP", + "name": "Gauteng" + }, + { + "code": "KZN", + "name": "KwaZulu-Natal" + }, + { + "code": "LP", + "name": "Limpopo" + }, + { + "code": "MP", + "name": "Mpumalanga" + }, + { + "code": "NC", + "name": "Northern Cape" + }, + { + "code": "NW", + "name": "North West" + }, + { + "code": "WC", + "name": "Western Cape" + } + ] + }, + { + "code": "ZM", + "name": "Zambian kwacha", + "currency_code": "ZMW", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ZM-01", + "name": "Western" + }, + { + "code": "ZM-02", + "name": "Central" + }, + { + "code": "ZM-03", + "name": "Eastern" + }, + { + "code": "ZM-04", + "name": "Luapula" + }, + { + "code": "ZM-05", + "name": "Northern" + }, + { + "code": "ZM-06", + "name": "North-Western" + }, + { + "code": "ZM-07", + "name": "Southern" + }, + { + "code": "ZM-08", + "name": "Copperbelt" + }, + { + "code": "ZM-09", + "name": "Lusaka" + }, + { + "code": "ZM-10", + "name": "Muchinga" + } + ] + }, + { + "code": "ZW", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "AN", + "name": "Antarctica", + "countries": [{ + "code": "AQ", + "name": "Antarctica", + "states": [] + }, + { + "code": "BV", + "name": "Bouvet Island", + "states": [] + }, + { + "code": "GS", + "name": "South Georgia/Sandwich Islands", + "states": [] + }, + { + "code": "HM", + "name": "Heard Island and McDonald Islands", + "states": [] + }, + { + "code": "TF", + "name": "French Southern Territories", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "AS", + "name": "Asia", + "countries": [{ + "code": "AE", + "name": "United Arab Emirates dirham", + "currency_code": "AED", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AF", + "name": "Afghan afghani", + "currency_code": "AFN", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AM", + "name": "Armenian dram", + "currency_code": "AMD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AZ", + "name": "Azerbaijani manat", + "currency_code": "AZN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BD", + "name": "Bangladeshi taka", + "currency_code": "BDT", + "currency_pos": "right", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "BD-05", + "name": "Bagerhat" + }, + { + "code": "BD-01", + "name": "Bandarban" + }, + { + "code": "BD-02", + "name": "Barguna" + }, + { + "code": "BD-06", + "name": "Barishal" + }, + { + "code": "BD-07", + "name": "Bhola" + }, + { + "code": "BD-03", + "name": "Bogura" + }, + { + "code": "BD-04", + "name": "Brahmanbaria" + }, + { + "code": "BD-09", + "name": "Chandpur" + }, + { + "code": "BD-10", + "name": "Chattogram" + }, + { + "code": "BD-12", + "name": "Chuadanga" + }, + { + "code": "BD-11", + "name": "Cox's Bazar" + }, + { + "code": "BD-08", + "name": "Cumilla" + }, + { + "code": "BD-13", + "name": "Dhaka" + }, + { + "code": "BD-14", + "name": "Dinajpur" + }, + { + "code": "BD-15", + "name": "Faridpur " + }, + { + "code": "BD-16", + "name": "Feni" + }, + { + "code": "BD-19", + "name": "Gaibandha" + }, + { + "code": "BD-18", + "name": "Gazipur" + }, + { + "code": "BD-17", + "name": "Gopalganj" + }, + { + "code": "BD-20", + "name": "Habiganj" + }, + { + "code": "BD-21", + "name": "Jamalpur" + }, + { + "code": "BD-22", + "name": "Jashore" + }, + { + "code": "BD-25", + "name": "Jhalokati" + }, + { + "code": "BD-23", + "name": "Jhenaidah" + }, + { + "code": "BD-24", + "name": "Joypurhat" + }, + { + "code": "BD-29", + "name": "Khagrachhari" + }, + { + "code": "BD-27", + "name": "Khulna" + }, + { + "code": "BD-26", + "name": "Kishoreganj" + }, + { + "code": "BD-28", + "name": "Kurigram" + }, + { + "code": "BD-30", + "name": "Kushtia" + }, + { + "code": "BD-31", + "name": "Lakshmipur" + }, + { + "code": "BD-32", + "name": "Lalmonirhat" + }, + { + "code": "BD-36", + "name": "Madaripur" + }, + { + "code": "BD-37", + "name": "Magura" + }, + { + "code": "BD-33", + "name": "Manikganj " + }, + { + "code": "BD-39", + "name": "Meherpur" + }, + { + "code": "BD-38", + "name": "Moulvibazar" + }, + { + "code": "BD-35", + "name": "Munshiganj" + }, + { + "code": "BD-34", + "name": "Mymensingh" + }, + { + "code": "BD-48", + "name": "Naogaon" + }, + { + "code": "BD-43", + "name": "Narail" + }, + { + "code": "BD-40", + "name": "Narayanganj" + }, + { + "code": "BD-42", + "name": "Narsingdi" + }, + { + "code": "BD-44", + "name": "Natore" + }, + { + "code": "BD-45", + "name": "Nawabganj" + }, + { + "code": "BD-41", + "name": "Netrakona" + }, + { + "code": "BD-46", + "name": "Nilphamari" + }, + { + "code": "BD-47", + "name": "Noakhali" + }, + { + "code": "BD-49", + "name": "Pabna" + }, + { + "code": "BD-52", + "name": "Panchagarh" + }, + { + "code": "BD-51", + "name": "Patuakhali" + }, + { + "code": "BD-50", + "name": "Pirojpur" + }, + { + "code": "BD-53", + "name": "Rajbari" + }, + { + "code": "BD-54", + "name": "Rajshahi" + }, + { + "code": "BD-56", + "name": "Rangamati" + }, + { + "code": "BD-55", + "name": "Rangpur" + }, + { + "code": "BD-58", + "name": "Satkhira" + }, + { + "code": "BD-62", + "name": "Shariatpur" + }, + { + "code": "BD-57", + "name": "Sherpur" + }, + { + "code": "BD-59", + "name": "Sirajganj" + }, + { + "code": "BD-61", + "name": "Sunamganj" + }, + { + "code": "BD-60", + "name": "Sylhet" + }, + { + "code": "BD-63", + "name": "Tangail" + }, + { + "code": "BD-64", + "name": "Thakurgaon" + } + ] + }, + { + "code": "BH", + "name": "Bahraini dinar", + "currency_code": "BHD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BN", + "name": "Brunei dollar", + "currency_code": "BND", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BT", + "name": "Bhutanese ngultrum", + "currency_code": "BTN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CC", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CN", + "name": "Chinese yuan", + "currency_code": "CNY", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CN1", + "name": "Yunnan / 云南" + }, + { + "code": "CN2", + "name": "Beijing / 北京" + }, + { + "code": "CN3", + "name": "Tianjin / 天津" + }, + { + "code": "CN4", + "name": "Hebei / 河北" + }, + { + "code": "CN5", + "name": "Shanxi / 山西" + }, + { + "code": "CN6", + "name": "Inner Mongolia / 內蒙古" + }, + { + "code": "CN7", + "name": "Liaoning / 辽宁" + }, + { + "code": "CN8", + "name": "Jilin / 吉林" + }, + { + "code": "CN9", + "name": "Heilongjiang / 黑龙江" + }, + { + "code": "CN10", + "name": "Shanghai / 上海" + }, + { + "code": "CN11", + "name": "Jiangsu / 江苏" + }, + { + "code": "CN12", + "name": "Zhejiang / 浙江" + }, + { + "code": "CN13", + "name": "Anhui / 安徽" + }, + { + "code": "CN14", + "name": "Fujian / 福建" + }, + { + "code": "CN15", + "name": "Jiangxi / 江西" + }, + { + "code": "CN16", + "name": "Shandong / 山东" + }, + { + "code": "CN17", + "name": "Henan / 河南" + }, + { + "code": "CN18", + "name": "Hubei / 湖北" + }, + { + "code": "CN19", + "name": "Hunan / 湖南" + }, + { + "code": "CN20", + "name": "Guangdong / 广东" + }, + { + "code": "CN21", + "name": "Guangxi Zhuang / 广西壮族" + }, + { + "code": "CN22", + "name": "Hainan / 海南" + }, + { + "code": "CN23", + "name": "Chongqing / 重庆" + }, + { + "code": "CN24", + "name": "Sichuan / 四川" + }, + { + "code": "CN25", + "name": "Guizhou / 贵州" + }, + { + "code": "CN26", + "name": "Shaanxi / 陕西" + }, + { + "code": "CN27", + "name": "Gansu / 甘肃" + }, + { + "code": "CN28", + "name": "Qinghai / 青海" + }, + { + "code": "CN29", + "name": "Ningxia Hui / 宁夏" + }, + { + "code": "CN30", + "name": "Macao / 澳门" + }, + { + "code": "CN31", + "name": "Tibet / 西藏" + }, + { + "code": "CN32", + "name": "Xinjiang / 新疆" + } + ] + }, + { + "code": "CX", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CY", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GE", + "name": "Georgian lari", + "currency_code": "GEL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "HK", + "name": "Hong Kong dollar", + "currency_code": "HKD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "HONG KONG", + "name": "Hong Kong Island" + }, + { + "code": "KOWLOON", + "name": "Kowloon" + }, + { + "code": "NEW TERRITORIES", + "name": "New Territories" + } + ] + }, + { + "code": "ID", + "name": "Indonesian rupiah", + "currency_code": "IDR", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AC", + "name": "Daerah Istimewa Aceh" + }, + { + "code": "SU", + "name": "Sumatera Utara" + }, + { + "code": "SB", + "name": "Sumatera Barat" + }, + { + "code": "RI", + "name": "Riau" + }, + { + "code": "KR", + "name": "Kepulauan Riau" + }, + { + "code": "JA", + "name": "Jambi" + }, + { + "code": "SS", + "name": "Sumatera Selatan" + }, + { + "code": "BB", + "name": "Bangka Belitung" + }, + { + "code": "BE", + "name": "Bengkulu" + }, + { + "code": "LA", + "name": "Lampung" + }, + { + "code": "JK", + "name": "DKI Jakarta" + }, + { + "code": "JB", + "name": "Jawa Barat" + }, + { + "code": "BT", + "name": "Banten" + }, + { + "code": "JT", + "name": "Jawa Tengah" + }, + { + "code": "JI", + "name": "Jawa Timur" + }, + { + "code": "YO", + "name": "Daerah Istimewa Yogyakarta" + }, + { + "code": "BA", + "name": "Bali" + }, + { + "code": "NB", + "name": "Nusa Tenggara Barat" + }, + { + "code": "NT", + "name": "Nusa Tenggara Timur" + }, + { + "code": "KB", + "name": "Kalimantan Barat" + }, + { + "code": "KT", + "name": "Kalimantan Tengah" + }, + { + "code": "KI", + "name": "Kalimantan Timur" + }, + { + "code": "KS", + "name": "Kalimantan Selatan" + }, + { + "code": "KU", + "name": "Kalimantan Utara" + }, + { + "code": "SA", + "name": "Sulawesi Utara" + }, + { + "code": "ST", + "name": "Sulawesi Tengah" + }, + { + "code": "SG", + "name": "Sulawesi Tenggara" + }, + { + "code": "SR", + "name": "Sulawesi Barat" + }, + { + "code": "SN", + "name": "Sulawesi Selatan" + }, + { + "code": "GO", + "name": "Gorontalo" + }, + { + "code": "MA", + "name": "Maluku" + }, + { + "code": "MU", + "name": "Maluku Utara" + }, + { + "code": "PA", + "name": "Papua" + }, + { + "code": "PB", + "name": "Papua Barat" + } + ] + }, + { + "code": "IL", + "name": "Israeli new shekel", + "currency_code": "ILS", + "currency_pos": "right_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IN", + "name": "Indian rupee", + "currency_code": "INR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AP", + "name": "Andhra Pradesh" + }, + { + "code": "AR", + "name": "Arunachal Pradesh" + }, + { + "code": "AS", + "name": "Assam" + }, + { + "code": "BR", + "name": "Bihar" + }, + { + "code": "CT", + "name": "Chhattisgarh" + }, + { + "code": "GA", + "name": "Goa" + }, + { + "code": "GJ", + "name": "Gujarat" + }, + { + "code": "HR", + "name": "Haryana" + }, + { + "code": "HP", + "name": "Himachal Pradesh" + }, + { + "code": "JK", + "name": "Jammu and Kashmir" + }, + { + "code": "JH", + "name": "Jharkhand" + }, + { + "code": "KA", + "name": "Karnataka" + }, + { + "code": "KL", + "name": "Kerala" + }, + { + "code": "LA", + "name": "Ladakh" + }, + { + "code": "MP", + "name": "Madhya Pradesh" + }, + { + "code": "MH", + "name": "Maharashtra" + }, + { + "code": "MN", + "name": "Manipur" + }, + { + "code": "ML", + "name": "Meghalaya" + }, + { + "code": "MZ", + "name": "Mizoram" + }, + { + "code": "NL", + "name": "Nagaland" + }, + { + "code": "OR", + "name": "Odisha" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "RJ", + "name": "Rajasthan" + }, + { + "code": "SK", + "name": "Sikkim" + }, + { + "code": "TN", + "name": "Tamil Nadu" + }, + { + "code": "TS", + "name": "Telangana" + }, + { + "code": "TR", + "name": "Tripura" + }, + { + "code": "UK", + "name": "Uttarakhand" + }, + { + "code": "UP", + "name": "Uttar Pradesh" + }, + { + "code": "WB", + "name": "West Bengal" + }, + { + "code": "AN", + "name": "Andaman and Nicobar Islands" + }, + { + "code": "CH", + "name": "Chandigarh" + }, + { + "code": "DN", + "name": "Dadra and Nagar Haveli" + }, + { + "code": "DD", + "name": "Daman and Diu" + }, + { + "code": "DL", + "name": "Delhi" + }, + { + "code": "LD", + "name": "Lakshadeep" + }, + { + "code": "PY", + "name": "Pondicherry (Puducherry)" + } + ] + }, + { + "code": "IO", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IQ", + "name": "Iraqi dinar", + "currency_code": "IQD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IR", + "name": "Iranian rial", + "currency_code": "IRR", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "KHZ", + "name": "Khuzestan (خوزستان)" + }, + { + "code": "THR", + "name": "Tehran (تهران)" + }, + { + "code": "ILM", + "name": "Ilaam (ایلام)" + }, + { + "code": "BHR", + "name": "Bushehr (بوشهر)" + }, + { + "code": "ADL", + "name": "Ardabil (اردبیل)" + }, + { + "code": "ESF", + "name": "Isfahan (اصفهان)" + }, + { + "code": "YZD", + "name": "Yazd (یزد)" + }, + { + "code": "KRH", + "name": "Kermanshah (کرمانشاه)" + }, + { + "code": "KRN", + "name": "Kerman (کرمان)" + }, + { + "code": "HDN", + "name": "Hamadan (همدان)" + }, + { + "code": "GZN", + "name": "Ghazvin (قزوین)" + }, + { + "code": "ZJN", + "name": "Zanjan (زنجان)" + }, + { + "code": "LRS", + "name": "Luristan (لرستان)" + }, + { + "code": "ABZ", + "name": "Alborz (البرز)" + }, + { + "code": "EAZ", + "name": "East Azarbaijan (آذربایجان شرقی)" + }, + { + "code": "WAZ", + "name": "West Azarbaijan (آذربایجان غربی)" + }, + { + "code": "CHB", + "name": "Chaharmahal and Bakhtiari (چهارمحال و بختیاری)" + }, + { + "code": "SKH", + "name": "South Khorasan (خراسان جنوبی)" + }, + { + "code": "RKH", + "name": "Razavi Khorasan (خراسان رضوی)" + }, + { + "code": "NKH", + "name": "North Khorasan (خراسان شمالی)" + }, + { + "code": "SMN", + "name": "Semnan (سمنان)" + }, + { + "code": "FRS", + "name": "Fars (فارس)" + }, + { + "code": "QHM", + "name": "Qom (قم)" + }, + { + "code": "KRD", + "name": "Kurdistan / کردستان)" + }, + { + "code": "KBD", + "name": "Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)" + }, + { + "code": "GLS", + "name": "Golestan (گلستان)" + }, + { + "code": "GIL", + "name": "Gilan (گیلان)" + }, + { + "code": "MZN", + "name": "Mazandaran (مازندران)" + }, + { + "code": "MKZ", + "name": "Markazi (مرکزی)" + }, + { + "code": "HRZ", + "name": "Hormozgan (هرمزگان)" + }, + { + "code": "SBN", + "name": "Sistan and Baluchestan (سیستان و بلوچستان)" + } + ] + }, + { + "code": "JO", + "name": "Jordanian dinar", + "currency_code": "JOD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "JP", + "name": "Japanese yen", + "currency_code": "JPY", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JP01", + "name": "Hokkaido" + }, + { + "code": "JP02", + "name": "Aomori" + }, + { + "code": "JP03", + "name": "Iwate" + }, + { + "code": "JP04", + "name": "Miyagi" + }, + { + "code": "JP05", + "name": "Akita" + }, + { + "code": "JP06", + "name": "Yamagata" + }, + { + "code": "JP07", + "name": "Fukushima" + }, + { + "code": "JP08", + "name": "Ibaraki" + }, + { + "code": "JP09", + "name": "Tochigi" + }, + { + "code": "JP10", + "name": "Gunma" + }, + { + "code": "JP11", + "name": "Saitama" + }, + { + "code": "JP12", + "name": "Chiba" + }, + { + "code": "JP13", + "name": "Tokyo" + }, + { + "code": "JP14", + "name": "Kanagawa" + }, + { + "code": "JP15", + "name": "Niigata" + }, + { + "code": "JP16", + "name": "Toyama" + }, + { + "code": "JP17", + "name": "Ishikawa" + }, + { + "code": "JP18", + "name": "Fukui" + }, + { + "code": "JP19", + "name": "Yamanashi" + }, + { + "code": "JP20", + "name": "Nagano" + }, + { + "code": "JP21", + "name": "Gifu" + }, + { + "code": "JP22", + "name": "Shizuoka" + }, + { + "code": "JP23", + "name": "Aichi" + }, + { + "code": "JP24", + "name": "Mie" + }, + { + "code": "JP25", + "name": "Shiga" + }, + { + "code": "JP26", + "name": "Kyoto" + }, + { + "code": "JP27", + "name": "Osaka" + }, + { + "code": "JP28", + "name": "Hyogo" + }, + { + "code": "JP29", + "name": "Nara" + }, + { + "code": "JP30", + "name": "Wakayama" + }, + { + "code": "JP31", + "name": "Tottori" + }, + { + "code": "JP32", + "name": "Shimane" + }, + { + "code": "JP33", + "name": "Okayama" + }, + { + "code": "JP34", + "name": "Hiroshima" + }, + { + "code": "JP35", + "name": "Yamaguchi" + }, + { + "code": "JP36", + "name": "Tokushima" + }, + { + "code": "JP37", + "name": "Kagawa" + }, + { + "code": "JP38", + "name": "Ehime" + }, + { + "code": "JP39", + "name": "Kochi" + }, + { + "code": "JP40", + "name": "Fukuoka" + }, + { + "code": "JP41", + "name": "Saga" + }, + { + "code": "JP42", + "name": "Nagasaki" + }, + { + "code": "JP43", + "name": "Kumamoto" + }, + { + "code": "JP44", + "name": "Oita" + }, + { + "code": "JP45", + "name": "Miyazaki" + }, + { + "code": "JP46", + "name": "Kagoshima" + }, + { + "code": "JP47", + "name": "Okinawa" + } + ] + }, + { + "code": "KG", + "name": "Kyrgyzstani som", + "currency_code": "KGS", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KH", + "name": "Cambodian riel", + "currency_code": "KHR", + "currency_pos": "right", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KP", + "name": "North Korean won", + "currency_code": "KPW", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KR", + "name": "South Korean won", + "currency_code": "KRW", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KW", + "name": "Kuwaiti dinar", + "currency_code": "KWD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KZ", + "name": "Kazakhstani tenge", + "currency_code": "KZT", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LA", + "name": "Lao kip", + "currency_code": "LAK", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AT", + "name": "Attapeu" + }, + { + "code": "BK", + "name": "Bokeo" + }, + { + "code": "BL", + "name": "Bolikhamsai" + }, + { + "code": "CH", + "name": "Champasak" + }, + { + "code": "HO", + "name": "Houaphanh" + }, + { + "code": "KH", + "name": "Khammouane" + }, + { + "code": "LM", + "name": "Luang Namtha" + }, + { + "code": "LP", + "name": "Luang Prabang" + }, + { + "code": "OU", + "name": "Oudomxay" + }, + { + "code": "PH", + "name": "Phongsaly" + }, + { + "code": "SL", + "name": "Salavan" + }, + { + "code": "SV", + "name": "Savannakhet" + }, + { + "code": "VI", + "name": "Vientiane Province" + }, + { + "code": "VT", + "name": "Vientiane" + }, + { + "code": "XA", + "name": "Sainyabuli" + }, + { + "code": "XE", + "name": "Sekong" + }, + { + "code": "XI", + "name": "Xiangkhouang" + }, + { + "code": "XS", + "name": "Xaisomboun" + } + ] + }, + { + "code": "LB", + "name": "Lebanese pound", + "currency_code": "LBP", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LK", + "name": "Sri Lankan rupee", + "currency_code": "LKR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MM", + "name": "Burmese kyat", + "currency_code": "MMK", + "currency_pos": "right_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MN", + "name": "Mongolian tögrög", + "currency_code": "MNT", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MO", + "name": "Macanese pataca", + "currency_code": "MOP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MV", + "name": "Maldivian rufiyaa", + "currency_code": "MVR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MY", + "name": "Malaysian ringgit", + "currency_code": "MYR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JHR", + "name": "Johor" + }, + { + "code": "KDH", + "name": "Kedah" + }, + { + "code": "KTN", + "name": "Kelantan" + }, + { + "code": "LBN", + "name": "Labuan" + }, + { + "code": "MLK", + "name": "Malacca (Melaka)" + }, + { + "code": "NSN", + "name": "Negeri Sembilan" + }, + { + "code": "PHG", + "name": "Pahang" + }, + { + "code": "PNG", + "name": "Penang (Pulau Pinang)" + }, + { + "code": "PRK", + "name": "Perak" + }, + { + "code": "PLS", + "name": "Perlis" + }, + { + "code": "SBH", + "name": "Sabah" + }, + { + "code": "SWK", + "name": "Sarawak" + }, + { + "code": "SGR", + "name": "Selangor" + }, + { + "code": "TRG", + "name": "Terengganu" + }, + { + "code": "PJY", + "name": "Putrajaya" + }, + { + "code": "KUL", + "name": "Kuala Lumpur" + } + ] + }, + { + "code": "NP", + "name": "Nepalese rupee", + "currency_code": "NPR", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "BAG", + "name": "Bagmati" + }, + { + "code": "BHE", + "name": "Bheri" + }, + { + "code": "DHA", + "name": "Dhaulagiri" + }, + { + "code": "GAN", + "name": "Gandaki" + }, + { + "code": "JAN", + "name": "Janakpur" + }, + { + "code": "KAR", + "name": "Karnali" + }, + { + "code": "KOS", + "name": "Koshi" + }, + { + "code": "LUM", + "name": "Lumbini" + }, + { + "code": "MAH", + "name": "Mahakali" + }, + { + "code": "MEC", + "name": "Mechi" + }, + { + "code": "NAR", + "name": "Narayani" + }, + { + "code": "RAP", + "name": "Rapti" + }, + { + "code": "SAG", + "name": "Sagarmatha" + }, + { + "code": "SET", + "name": "Seti" + } + ] + }, + { + "code": "OM", + "name": "Omani rial", + "currency_code": "OMR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PH", + "name": "Philippine peso", + "currency_code": "PHP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ABR", + "name": "Abra" + }, + { + "code": "AGN", + "name": "Agusan del Norte" + }, + { + "code": "AGS", + "name": "Agusan del Sur" + }, + { + "code": "AKL", + "name": "Aklan" + }, + { + "code": "ALB", + "name": "Albay" + }, + { + "code": "ANT", + "name": "Antique" + }, + { + "code": "APA", + "name": "Apayao" + }, + { + "code": "AUR", + "name": "Aurora" + }, + { + "code": "BAS", + "name": "Basilan" + }, + { + "code": "BAN", + "name": "Bataan" + }, + { + "code": "BTN", + "name": "Batanes" + }, + { + "code": "BTG", + "name": "Batangas" + }, + { + "code": "BEN", + "name": "Benguet" + }, + { + "code": "BIL", + "name": "Biliran" + }, + { + "code": "BOH", + "name": "Bohol" + }, + { + "code": "BUK", + "name": "Bukidnon" + }, + { + "code": "BUL", + "name": "Bulacan" + }, + { + "code": "CAG", + "name": "Cagayan" + }, + { + "code": "CAN", + "name": "Camarines Norte" + }, + { + "code": "CAS", + "name": "Camarines Sur" + }, + { + "code": "CAM", + "name": "Camiguin" + }, + { + "code": "CAP", + "name": "Capiz" + }, + { + "code": "CAT", + "name": "Catanduanes" + }, + { + "code": "CAV", + "name": "Cavite" + }, + { + "code": "CEB", + "name": "Cebu" + }, + { + "code": "COM", + "name": "Compostela Valley" + }, + { + "code": "NCO", + "name": "Cotabato" + }, + { + "code": "DAV", + "name": "Davao del Norte" + }, + { + "code": "DAS", + "name": "Davao del Sur" + }, + { + "code": "DAC", + "name": "Davao Occidental" + }, + { + "code": "DAO", + "name": "Davao Oriental" + }, + { + "code": "DIN", + "name": "Dinagat Islands" + }, + { + "code": "EAS", + "name": "Eastern Samar" + }, + { + "code": "GUI", + "name": "Guimaras" + }, + { + "code": "IFU", + "name": "Ifugao" + }, + { + "code": "ILN", + "name": "Ilocos Norte" + }, + { + "code": "ILS", + "name": "Ilocos Sur" + }, + { + "code": "ILI", + "name": "Iloilo" + }, + { + "code": "ISA", + "name": "Isabela" + }, + { + "code": "KAL", + "name": "Kalinga" + }, + { + "code": "LUN", + "name": "La Union" + }, + { + "code": "LAG", + "name": "Laguna" + }, + { + "code": "LAN", + "name": "Lanao del Norte" + }, + { + "code": "LAS", + "name": "Lanao del Sur" + }, + { + "code": "LEY", + "name": "Leyte" + }, + { + "code": "MAG", + "name": "Maguindanao" + }, + { + "code": "MAD", + "name": "Marinduque" + }, + { + "code": "MAS", + "name": "Masbate" + }, + { + "code": "MSC", + "name": "Misamis Occidental" + }, + { + "code": "MSR", + "name": "Misamis Oriental" + }, + { + "code": "MOU", + "name": "Mountain Province" + }, + { + "code": "NEC", + "name": "Negros Occidental" + }, + { + "code": "NER", + "name": "Negros Oriental" + }, + { + "code": "NSA", + "name": "Northern Samar" + }, + { + "code": "NUE", + "name": "Nueva Ecija" + }, + { + "code": "NUV", + "name": "Nueva Vizcaya" + }, + { + "code": "MDC", + "name": "Occidental Mindoro" + }, + { + "code": "MDR", + "name": "Oriental Mindoro" + }, + { + "code": "PLW", + "name": "Palawan" + }, + { + "code": "PAM", + "name": "Pampanga" + }, + { + "code": "PAN", + "name": "Pangasinan" + }, + { + "code": "QUE", + "name": "Quezon" + }, + { + "code": "QUI", + "name": "Quirino" + }, + { + "code": "RIZ", + "name": "Rizal" + }, + { + "code": "ROM", + "name": "Romblon" + }, + { + "code": "WSA", + "name": "Samar" + }, + { + "code": "SAR", + "name": "Sarangani" + }, + { + "code": "SIQ", + "name": "Siquijor" + }, + { + "code": "SOR", + "name": "Sorsogon" + }, + { + "code": "SCO", + "name": "South Cotabato" + }, + { + "code": "SLE", + "name": "Southern Leyte" + }, + { + "code": "SUK", + "name": "Sultan Kudarat" + }, + { + "code": "SLU", + "name": "Sulu" + }, + { + "code": "SUN", + "name": "Surigao del Norte" + }, + { + "code": "SUR", + "name": "Surigao del Sur" + }, + { + "code": "TAR", + "name": "Tarlac" + }, + { + "code": "TAW", + "name": "Tawi-Tawi" + }, + { + "code": "ZMB", + "name": "Zambales" + }, + { + "code": "ZAN", + "name": "Zamboanga del Norte" + }, + { + "code": "ZAS", + "name": "Zamboanga del Sur" + }, + { + "code": "ZSI", + "name": "Zamboanga Sibugay" + }, + { + "code": "00", + "name": "Metro Manila" + } + ] + }, + { + "code": "PK", + "name": "Pakistani rupee", + "currency_code": "PKR", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JK", + "name": "Azad Kashmir" + }, + { + "code": "BA", + "name": "Balochistan" + }, + { + "code": "TA", + "name": "FATA" + }, + { + "code": "GB", + "name": "Gilgit Baltistan" + }, + { + "code": "IS", + "name": "Islamabad Capital Territory" + }, + { + "code": "KP", + "name": "Khyber Pakhtunkhwa" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "SD", + "name": "Sindh" + } + ] + }, + { + "code": "PS", + "name": "Jordanian dinar", + "currency_code": "JOD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "QA", + "name": "Qatari riyal", + "currency_code": "QAR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SA", + "name": "Saudi riyal", + "currency_code": "SAR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SG", + "name": "Singapore dollar", + "currency_code": "SGD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SY", + "name": "Syrian pound", + "currency_code": "SYP", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TH", + "name": "Thai baht", + "currency_code": "THB", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "TH-37", + "name": "Amnat Charoen" + }, + { + "code": "TH-15", + "name": "Ang Thong" + }, + { + "code": "TH-14", + "name": "Ayutthaya" + }, + { + "code": "TH-10", + "name": "Bangkok" + }, + { + "code": "TH-38", + "name": "Bueng Kan" + }, + { + "code": "TH-31", + "name": "Buri Ram" + }, + { + "code": "TH-24", + "name": "Chachoengsao" + }, + { + "code": "TH-18", + "name": "Chai Nat" + }, + { + "code": "TH-36", + "name": "Chaiyaphum" + }, + { + "code": "TH-22", + "name": "Chanthaburi" + }, + { + "code": "TH-50", + "name": "Chiang Mai" + }, + { + "code": "TH-57", + "name": "Chiang Rai" + }, + { + "code": "TH-20", + "name": "Chonburi" + }, + { + "code": "TH-86", + "name": "Chumphon" + }, + { + "code": "TH-46", + "name": "Kalasin" + }, + { + "code": "TH-62", + "name": "Kamphaeng Phet" + }, + { + "code": "TH-71", + "name": "Kanchanaburi" + }, + { + "code": "TH-40", + "name": "Khon Kaen" + }, + { + "code": "TH-81", + "name": "Krabi" + }, + { + "code": "TH-52", + "name": "Lampang" + }, + { + "code": "TH-51", + "name": "Lamphun" + }, + { + "code": "TH-42", + "name": "Loei" + }, + { + "code": "TH-16", + "name": "Lopburi" + }, + { + "code": "TH-58", + "name": "Mae Hong Son" + }, + { + "code": "TH-44", + "name": "Maha Sarakham" + }, + { + "code": "TH-49", + "name": "Mukdahan" + }, + { + "code": "TH-26", + "name": "Nakhon Nayok" + }, + { + "code": "TH-73", + "name": "Nakhon Pathom" + }, + { + "code": "TH-48", + "name": "Nakhon Phanom" + }, + { + "code": "TH-30", + "name": "Nakhon Ratchasima" + }, + { + "code": "TH-60", + "name": "Nakhon Sawan" + }, + { + "code": "TH-80", + "name": "Nakhon Si Thammarat" + }, + { + "code": "TH-55", + "name": "Nan" + }, + { + "code": "TH-96", + "name": "Narathiwat" + }, + { + "code": "TH-39", + "name": "Nong Bua Lam Phu" + }, + { + "code": "TH-43", + "name": "Nong Khai" + }, + { + "code": "TH-12", + "name": "Nonthaburi" + }, + { + "code": "TH-13", + "name": "Pathum Thani" + }, + { + "code": "TH-94", + "name": "Pattani" + }, + { + "code": "TH-82", + "name": "Phang Nga" + }, + { + "code": "TH-93", + "name": "Phatthalung" + }, + { + "code": "TH-56", + "name": "Phayao" + }, + { + "code": "TH-67", + "name": "Phetchabun" + }, + { + "code": "TH-76", + "name": "Phetchaburi" + }, + { + "code": "TH-66", + "name": "Phichit" + }, + { + "code": "TH-65", + "name": "Phitsanulok" + }, + { + "code": "TH-54", + "name": "Phrae" + }, + { + "code": "TH-83", + "name": "Phuket" + }, + { + "code": "TH-25", + "name": "Prachin Buri" + }, + { + "code": "TH-77", + "name": "Prachuap Khiri Khan" + }, + { + "code": "TH-85", + "name": "Ranong" + }, + { + "code": "TH-70", + "name": "Ratchaburi" + }, + { + "code": "TH-21", + "name": "Rayong" + }, + { + "code": "TH-45", + "name": "Roi Et" + }, + { + "code": "TH-27", + "name": "Sa Kaeo" + }, + { + "code": "TH-47", + "name": "Sakon Nakhon" + }, + { + "code": "TH-11", + "name": "Samut Prakan" + }, + { + "code": "TH-74", + "name": "Samut Sakhon" + }, + { + "code": "TH-75", + "name": "Samut Songkhram" + }, + { + "code": "TH-19", + "name": "Saraburi" + }, + { + "code": "TH-91", + "name": "Satun" + }, + { + "code": "TH-17", + "name": "Sing Buri" + }, + { + "code": "TH-33", + "name": "Sisaket" + }, + { + "code": "TH-90", + "name": "Songkhla" + }, + { + "code": "TH-64", + "name": "Sukhothai" + }, + { + "code": "TH-72", + "name": "Suphan Buri" + }, + { + "code": "TH-84", + "name": "Surat Thani" + }, + { + "code": "TH-32", + "name": "Surin" + }, + { + "code": "TH-63", + "name": "Tak" + }, + { + "code": "TH-92", + "name": "Trang" + }, + { + "code": "TH-23", + "name": "Trat" + }, + { + "code": "TH-34", + "name": "Ubon Ratchathani" + }, + { + "code": "TH-41", + "name": "Udon Thani" + }, + { + "code": "TH-61", + "name": "Uthai Thani" + }, + { + "code": "TH-53", + "name": "Uttaradit" + }, + { + "code": "TH-95", + "name": "Yala" + }, + { + "code": "TH-35", + "name": "Yasothon" + } + ] + }, + { + "code": "TJ", + "name": "Tajikistani somoni", + "currency_code": "TJS", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TL", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TM", + "name": "Turkmenistan manat", + "currency_code": "TMT", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TW", + "name": "New Taiwan dollar", + "currency_code": "TWD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "UZ", + "name": "Uzbekistani som", + "currency_code": "UZS", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "VN", + "name": "Vietnamese đồng", + "currency_code": "VND", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "YE", + "name": "Yemeni rial", + "currency_code": "YER", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "EU", + "name": "Europe", + "countries": [{ + "code": "AD", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AL", + "name": "Albanian lek", + "currency_code": "ALL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "AL-01", + "name": "Berat" + }, + { + "code": "AL-09", + "name": "Dibër" + }, + { + "code": "AL-02", + "name": "Durrës" + }, + { + "code": "AL-03", + "name": "Elbasan" + }, + { + "code": "AL-04", + "name": "Fier" + }, + { + "code": "AL-05", + "name": "Gjirokastër" + }, + { + "code": "AL-06", + "name": "Korçë" + }, + { + "code": "AL-07", + "name": "Kukës" + }, + { + "code": "AL-08", + "name": "Lezhë" + }, + { + "code": "AL-10", + "name": "Shkodër" + }, + { + "code": "AL-11", + "name": "Tirana" + }, + { + "code": "AL-12", + "name": "Vlorë" + } + ] + }, + { + "code": "AT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AX", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BA", + "name": "Bosnia and Herzegovina convertible mark", + "currency_code": "BAM", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BG", + "name": "Bulgarian lev", + "currency_code": "BGN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BG-01", + "name": "Blagoevgrad" + }, + { + "code": "BG-02", + "name": "Burgas" + }, + { + "code": "BG-08", + "name": "Dobrich" + }, + { + "code": "BG-07", + "name": "Gabrovo" + }, + { + "code": "BG-26", + "name": "Haskovo" + }, + { + "code": "BG-09", + "name": "Kardzhali" + }, + { + "code": "BG-10", + "name": "Kyustendil" + }, + { + "code": "BG-11", + "name": "Lovech" + }, + { + "code": "BG-12", + "name": "Montana" + }, + { + "code": "BG-13", + "name": "Pazardzhik" + }, + { + "code": "BG-14", + "name": "Pernik" + }, + { + "code": "BG-15", + "name": "Pleven" + }, + { + "code": "BG-16", + "name": "Plovdiv" + }, + { + "code": "BG-17", + "name": "Razgrad" + }, + { + "code": "BG-18", + "name": "Ruse" + }, + { + "code": "BG-27", + "name": "Shumen" + }, + { + "code": "BG-19", + "name": "Silistra" + }, + { + "code": "BG-20", + "name": "Sliven" + }, + { + "code": "BG-21", + "name": "Smolyan" + }, + { + "code": "BG-23", + "name": "Sofia District" + }, + { + "code": "BG-22", + "name": "Sofia" + }, + { + "code": "BG-24", + "name": "Stara Zagora" + }, + { + "code": "BG-25", + "name": "Targovishte" + }, + { + "code": "BG-03", + "name": "Varna" + }, + { + "code": "BG-04", + "name": "Veliko Tarnovo" + }, + { + "code": "BG-05", + "name": "Vidin" + }, + { + "code": "BG-06", + "name": "Vratsa" + }, + { + "code": "BG-28", + "name": "Yambol" + } + ] + }, + { + "code": "BY", + "name": "Belarusian ruble", + "currency_code": "BYN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CH", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Aargau" + }, + { + "code": "AR", + "name": "Appenzell Ausserrhoden" + }, + { + "code": "AI", + "name": "Appenzell Innerrhoden" + }, + { + "code": "BL", + "name": "Basel-Landschaft" + }, + { + "code": "BS", + "name": "Basel-Stadt" + }, + { + "code": "BE", + "name": "Bern" + }, + { + "code": "FR", + "name": "Fribourg" + }, + { + "code": "GE", + "name": "Geneva" + }, + { + "code": "GL", + "name": "Glarus" + }, + { + "code": "GR", + "name": "Graubünden" + }, + { + "code": "JU", + "name": "Jura" + }, + { + "code": "LU", + "name": "Luzern" + }, + { + "code": "NE", + "name": "Neuchâtel" + }, + { + "code": "NW", + "name": "Nidwalden" + }, + { + "code": "OW", + "name": "Obwalden" + }, + { + "code": "SH", + "name": "Schaffhausen" + }, + { + "code": "SZ", + "name": "Schwyz" + }, + { + "code": "SO", + "name": "Solothurn" + }, + { + "code": "SG", + "name": "St. Gallen" + }, + { + "code": "TG", + "name": "Thurgau" + }, + { + "code": "TI", + "name": "Ticino" + }, + { + "code": "UR", + "name": "Uri" + }, + { + "code": "VS", + "name": "Valais" + }, + { + "code": "VD", + "name": "Vaud" + }, + { + "code": "ZG", + "name": "Zug" + }, + { + "code": "ZH", + "name": "Zürich" + } + ] + }, + { + "code": "CZ", + "name": "Czech koruna", + "currency_code": "CZK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "DE-BW", + "name": "Baden-Württemberg" + }, + { + "code": "DE-BY", + "name": "Bavaria" + }, + { + "code": "DE-BE", + "name": "Berlin" + }, + { + "code": "DE-BB", + "name": "Brandenburg" + }, + { + "code": "DE-HB", + "name": "Bremen" + }, + { + "code": "DE-HH", + "name": "Hamburg" + }, + { + "code": "DE-HE", + "name": "Hesse" + }, + { + "code": "DE-MV", + "name": "Mecklenburg-Vorpommern" + }, + { + "code": "DE-NI", + "name": "Lower Saxony" + }, + { + "code": "DE-NW", + "name": "North Rhine-Westphalia" + }, + { + "code": "DE-RP", + "name": "Rhineland-Palatinate" + }, + { + "code": "DE-SL", + "name": "Saarland" + }, + { + "code": "DE-SN", + "name": "Saxony" + }, + { + "code": "DE-ST", + "name": "Saxony-Anhalt" + }, + { + "code": "DE-SH", + "name": "Schleswig-Holstein" + }, + { + "code": "DE-TH", + "name": "Thuringia" + } + ] + }, + { + "code": "DK", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "EE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ES", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "A Coruña" + }, + { + "code": "VI", + "name": "Araba/Álava" + }, + { + "code": "AB", + "name": "Albacete" + }, + { + "code": "A", + "name": "Alicante" + }, + { + "code": "AL", + "name": "Almería" + }, + { + "code": "O", + "name": "Asturias" + }, + { + "code": "AV", + "name": "Ávila" + }, + { + "code": "BA", + "name": "Badajoz" + }, + { + "code": "PM", + "name": "Baleares" + }, + { + "code": "B", + "name": "Barcelona" + }, + { + "code": "BU", + "name": "Burgos" + }, + { + "code": "CC", + "name": "Cáceres" + }, + { + "code": "CA", + "name": "Cádiz" + }, + { + "code": "S", + "name": "Cantabria" + }, + { + "code": "CS", + "name": "Castellón" + }, + { + "code": "CE", + "name": "Ceuta" + }, + { + "code": "CR", + "name": "Ciudad Real" + }, + { + "code": "CO", + "name": "Córdoba" + }, + { + "code": "CU", + "name": "Cuenca" + }, + { + "code": "GI", + "name": "Girona" + }, + { + "code": "GR", + "name": "Granada" + }, + { + "code": "GU", + "name": "Guadalajara" + }, + { + "code": "SS", + "name": "Gipuzkoa" + }, + { + "code": "H", + "name": "Huelva" + }, + { + "code": "HU", + "name": "Huesca" + }, + { + "code": "J", + "name": "Jaén" + }, + { + "code": "LO", + "name": "La Rioja" + }, + { + "code": "GC", + "name": "Las Palmas" + }, + { + "code": "LE", + "name": "León" + }, + { + "code": "L", + "name": "Lleida" + }, + { + "code": "LU", + "name": "Lugo" + }, + { + "code": "M", + "name": "Madrid" + }, + { + "code": "MA", + "name": "Málaga" + }, + { + "code": "ML", + "name": "Melilla" + }, + { + "code": "MU", + "name": "Murcia" + }, + { + "code": "NA", + "name": "Navarra" + }, + { + "code": "OR", + "name": "Ourense" + }, + { + "code": "P", + "name": "Palencia" + }, + { + "code": "PO", + "name": "Pontevedra" + }, + { + "code": "SA", + "name": "Salamanca" + }, + { + "code": "TF", + "name": "Santa Cruz de Tenerife" + }, + { + "code": "SG", + "name": "Segovia" + }, + { + "code": "SE", + "name": "Sevilla" + }, + { + "code": "SO", + "name": "Soria" + }, + { + "code": "T", + "name": "Tarragona" + }, + { + "code": "TE", + "name": "Teruel" + }, + { + "code": "TO", + "name": "Toledo" + }, + { + "code": "V", + "name": "Valencia" + }, + { + "code": "VA", + "name": "Valladolid" + }, + { + "code": "BI", + "name": "Biscay" + }, + { + "code": "ZA", + "name": "Zamora" + }, + { + "code": "Z", + "name": "Zaragoza" + } + ] + }, + { + "code": "FI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FO", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GB", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "foot", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "oz", + "states": [] + }, + { + "code": "GG", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GI", + "name": "Gibraltar pound", + "currency_code": "GIP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "I", + "name": "Attica" + }, + { + "code": "A", + "name": "East Macedonia and Thrace" + }, + { + "code": "B", + "name": "Central Macedonia" + }, + { + "code": "C", + "name": "West Macedonia" + }, + { + "code": "D", + "name": "Epirus" + }, + { + "code": "E", + "name": "Thessaly" + }, + { + "code": "F", + "name": "Ionian Islands" + }, + { + "code": "G", + "name": "West Greece" + }, + { + "code": "H", + "name": "Central Greece" + }, + { + "code": "J", + "name": "Peloponnese" + }, + { + "code": "K", + "name": "North Aegean" + }, + { + "code": "L", + "name": "South Aegean" + }, + { + "code": "M", + "name": "Crete" + } + ] + }, + { + "code": "HR", + "name": "Croatian kuna", + "currency_code": "HRK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "HU", + "name": "Hungarian forint", + "currency_code": "HUF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BK", + "name": "Bács-Kiskun" + }, + { + "code": "BE", + "name": "Békés" + }, + { + "code": "BA", + "name": "Baranya" + }, + { + "code": "BZ", + "name": "Borsod-Abaúj-Zemplén" + }, + { + "code": "BU", + "name": "Budapest" + }, + { + "code": "CS", + "name": "Csongrád-Csanád" + }, + { + "code": "FE", + "name": "Fejér" + }, + { + "code": "GS", + "name": "Győr-Moson-Sopron" + }, + { + "code": "HB", + "name": "Hajdú-Bihar" + }, + { + "code": "HE", + "name": "Heves" + }, + { + "code": "JN", + "name": "Jász-Nagykun-Szolnok" + }, + { + "code": "KE", + "name": "Komárom-Esztergom" + }, + { + "code": "NO", + "name": "Nógrád" + }, + { + "code": "PE", + "name": "Pest" + }, + { + "code": "SO", + "name": "Somogy" + }, + { + "code": "SZ", + "name": "Szabolcs-Szatmár-Bereg" + }, + { + "code": "TO", + "name": "Tolna" + }, + { + "code": "VA", + "name": "Vas" + }, + { + "code": "VE", + "name": "Veszprém" + }, + { + "code": "ZA", + "name": "Zala" + } + ] + }, + { + "code": "IE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CW", + "name": "Carlow" + }, + { + "code": "CN", + "name": "Cavan" + }, + { + "code": "CE", + "name": "Clare" + }, + { + "code": "CO", + "name": "Cork" + }, + { + "code": "DL", + "name": "Donegal" + }, + { + "code": "D", + "name": "Dublin" + }, + { + "code": "G", + "name": "Galway" + }, + { + "code": "KY", + "name": "Kerry" + }, + { + "code": "KE", + "name": "Kildare" + }, + { + "code": "KK", + "name": "Kilkenny" + }, + { + "code": "LS", + "name": "Laois" + }, + { + "code": "LM", + "name": "Leitrim" + }, + { + "code": "LK", + "name": "Limerick" + }, + { + "code": "LD", + "name": "Longford" + }, + { + "code": "LH", + "name": "Louth" + }, + { + "code": "MO", + "name": "Mayo" + }, + { + "code": "MH", + "name": "Meath" + }, + { + "code": "MN", + "name": "Monaghan" + }, + { + "code": "OY", + "name": "Offaly" + }, + { + "code": "RN", + "name": "Roscommon" + }, + { + "code": "SO", + "name": "Sligo" + }, + { + "code": "TA", + "name": "Tipperary" + }, + { + "code": "WD", + "name": "Waterford" + }, + { + "code": "WH", + "name": "Westmeath" + }, + { + "code": "WX", + "name": "Wexford" + }, + { + "code": "WW", + "name": "Wicklow" + } + ] + }, + { + "code": "IM", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IS", + "name": "Icelandic króna", + "currency_code": "ISK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Agrigento" + }, + { + "code": "AL", + "name": "Alessandria" + }, + { + "code": "AN", + "name": "Ancona" + }, + { + "code": "AO", + "name": "Aosta" + }, + { + "code": "AR", + "name": "Arezzo" + }, + { + "code": "AP", + "name": "Ascoli Piceno" + }, + { + "code": "AT", + "name": "Asti" + }, + { + "code": "AV", + "name": "Avellino" + }, + { + "code": "BA", + "name": "Bari" + }, + { + "code": "BT", + "name": "Barletta-Andria-Trani" + }, + { + "code": "BL", + "name": "Belluno" + }, + { + "code": "BN", + "name": "Benevento" + }, + { + "code": "BG", + "name": "Bergamo" + }, + { + "code": "BI", + "name": "Biella" + }, + { + "code": "BO", + "name": "Bologna" + }, + { + "code": "BZ", + "name": "Bolzano" + }, + { + "code": "BS", + "name": "Brescia" + }, + { + "code": "BR", + "name": "Brindisi" + }, + { + "code": "CA", + "name": "Cagliari" + }, + { + "code": "CL", + "name": "Caltanissetta" + }, + { + "code": "CB", + "name": "Campobasso" + }, + { + "code": "CE", + "name": "Caserta" + }, + { + "code": "CT", + "name": "Catania" + }, + { + "code": "CZ", + "name": "Catanzaro" + }, + { + "code": "CH", + "name": "Chieti" + }, + { + "code": "CO", + "name": "Como" + }, + { + "code": "CS", + "name": "Cosenza" + }, + { + "code": "CR", + "name": "Cremona" + }, + { + "code": "KR", + "name": "Crotone" + }, + { + "code": "CN", + "name": "Cuneo" + }, + { + "code": "EN", + "name": "Enna" + }, + { + "code": "FM", + "name": "Fermo" + }, + { + "code": "FE", + "name": "Ferrara" + }, + { + "code": "FI", + "name": "Firenze" + }, + { + "code": "FG", + "name": "Foggia" + }, + { + "code": "FC", + "name": "Forlì-Cesena" + }, + { + "code": "FR", + "name": "Frosinone" + }, + { + "code": "GE", + "name": "Genova" + }, + { + "code": "GO", + "name": "Gorizia" + }, + { + "code": "GR", + "name": "Grosseto" + }, + { + "code": "IM", + "name": "Imperia" + }, + { + "code": "IS", + "name": "Isernia" + }, + { + "code": "SP", + "name": "La Spezia" + }, + { + "code": "AQ", + "name": "L'Aquila" + }, + { + "code": "LT", + "name": "Latina" + }, + { + "code": "LE", + "name": "Lecce" + }, + { + "code": "LC", + "name": "Lecco" + }, + { + "code": "LI", + "name": "Livorno" + }, + { + "code": "LO", + "name": "Lodi" + }, + { + "code": "LU", + "name": "Lucca" + }, + { + "code": "MC", + "name": "Macerata" + }, + { + "code": "MN", + "name": "Mantova" + }, + { + "code": "MS", + "name": "Massa-Carrara" + }, + { + "code": "MT", + "name": "Matera" + }, + { + "code": "ME", + "name": "Messina" + }, + { + "code": "MI", + "name": "Milano" + }, + { + "code": "MO", + "name": "Modena" + }, + { + "code": "MB", + "name": "Monza e della Brianza" + }, + { + "code": "NA", + "name": "Napoli" + }, + { + "code": "NO", + "name": "Novara" + }, + { + "code": "NU", + "name": "Nuoro" + }, + { + "code": "OR", + "name": "Oristano" + }, + { + "code": "PD", + "name": "Padova" + }, + { + "code": "PA", + "name": "Palermo" + }, + { + "code": "PR", + "name": "Parma" + }, + { + "code": "PV", + "name": "Pavia" + }, + { + "code": "PG", + "name": "Perugia" + }, + { + "code": "PU", + "name": "Pesaro e Urbino" + }, + { + "code": "PE", + "name": "Pescara" + }, + { + "code": "PC", + "name": "Piacenza" + }, + { + "code": "PI", + "name": "Pisa" + }, + { + "code": "PT", + "name": "Pistoia" + }, + { + "code": "PN", + "name": "Pordenone" + }, + { + "code": "PZ", + "name": "Potenza" + }, + { + "code": "PO", + "name": "Prato" + }, + { + "code": "RG", + "name": "Ragusa" + }, + { + "code": "RA", + "name": "Ravenna" + }, + { + "code": "RC", + "name": "Reggio Calabria" + }, + { + "code": "RE", + "name": "Reggio Emilia" + }, + { + "code": "RI", + "name": "Rieti" + }, + { + "code": "RN", + "name": "Rimini" + }, + { + "code": "RM", + "name": "Roma" + }, + { + "code": "RO", + "name": "Rovigo" + }, + { + "code": "SA", + "name": "Salerno" + }, + { + "code": "SS", + "name": "Sassari" + }, + { + "code": "SV", + "name": "Savona" + }, + { + "code": "SI", + "name": "Siena" + }, + { + "code": "SR", + "name": "Siracusa" + }, + { + "code": "SO", + "name": "Sondrio" + }, + { + "code": "SU", + "name": "Sud Sardegna" + }, + { + "code": "TA", + "name": "Taranto" + }, + { + "code": "TE", + "name": "Teramo" + }, + { + "code": "TR", + "name": "Terni" + }, + { + "code": "TO", + "name": "Torino" + }, + { + "code": "TP", + "name": "Trapani" + }, + { + "code": "TN", + "name": "Trento" + }, + { + "code": "TV", + "name": "Treviso" + }, + { + "code": "TS", + "name": "Trieste" + }, + { + "code": "UD", + "name": "Udine" + }, + { + "code": "VA", + "name": "Varese" + }, + { + "code": "VE", + "name": "Venezia" + }, + { + "code": "VB", + "name": "Verbano-Cusio-Ossola" + }, + { + "code": "VC", + "name": "Vercelli" + }, + { + "code": "VR", + "name": "Verona" + }, + { + "code": "VV", + "name": "Vibo Valentia" + }, + { + "code": "VI", + "name": "Vicenza" + }, + { + "code": "VT", + "name": "Viterbo" + } + ] + }, + { + "code": "JE", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LI", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LU", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LV", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MC", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MD", + "name": "Moldovan leu", + "currency_code": "MDL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "Chișinău" + }, + { + "code": "BL", + "name": "Bălți" + }, + { + "code": "AN", + "name": "Anenii Noi" + }, + { + "code": "BS", + "name": "Basarabeasca" + }, + { + "code": "BR", + "name": "Briceni" + }, + { + "code": "CH", + "name": "Cahul" + }, + { + "code": "CT", + "name": "Cantemir" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Căușeni" + }, + { + "code": "CM", + "name": "Cimișlia" + }, + { + "code": "CR", + "name": "Criuleni" + }, + { + "code": "DN", + "name": "Dondușeni" + }, + { + "code": "DR", + "name": "Drochia" + }, + { + "code": "DB", + "name": "Dubăsari" + }, + { + "code": "ED", + "name": "Edineț" + }, + { + "code": "FL", + "name": "Fălești" + }, + { + "code": "FR", + "name": "Florești" + }, + { + "code": "GE", + "name": "UTA Găgăuzia" + }, + { + "code": "GL", + "name": "Glodeni" + }, + { + "code": "HN", + "name": "Hîncești" + }, + { + "code": "IL", + "name": "Ialoveni" + }, + { + "code": "LV", + "name": "Leova" + }, + { + "code": "NS", + "name": "Nisporeni" + }, + { + "code": "OC", + "name": "Ocnița" + }, + { + "code": "OR", + "name": "Orhei" + }, + { + "code": "RZ", + "name": "Rezina" + }, + { + "code": "RS", + "name": "Rîșcani" + }, + { + "code": "SG", + "name": "Sîngerei" + }, + { + "code": "SR", + "name": "Soroca" + }, + { + "code": "ST", + "name": "Strășeni" + }, + { + "code": "SD", + "name": "Șoldănești" + }, + { + "code": "SV", + "name": "Ștefan Vodă" + }, + { + "code": "TR", + "name": "Taraclia" + }, + { + "code": "TL", + "name": "Telenești" + }, + { + "code": "UN", + "name": "Ungheni" + } + ] + }, + { + "code": "ME", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MK", + "name": "Macedonian denar", + "currency_code": "MKD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NL", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NO", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PL", + "name": "Polish złoty", + "currency_code": "PLN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "RO", + "name": "Romanian leu", + "currency_code": "RON", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Alba" + }, + { + "code": "AR", + "name": "Arad" + }, + { + "code": "AG", + "name": "Argeș" + }, + { + "code": "BC", + "name": "Bacău" + }, + { + "code": "BH", + "name": "Bihor" + }, + { + "code": "BN", + "name": "Bistrița-Năsăud" + }, + { + "code": "BT", + "name": "Botoșani" + }, + { + "code": "BR", + "name": "Brăila" + }, + { + "code": "BV", + "name": "Brașov" + }, + { + "code": "B", + "name": "București" + }, + { + "code": "BZ", + "name": "Buzău" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Caraș-Severin" + }, + { + "code": "CJ", + "name": "Cluj" + }, + { + "code": "CT", + "name": "Constanța" + }, + { + "code": "CV", + "name": "Covasna" + }, + { + "code": "DB", + "name": "Dâmbovița" + }, + { + "code": "DJ", + "name": "Dolj" + }, + { + "code": "GL", + "name": "Galați" + }, + { + "code": "GR", + "name": "Giurgiu" + }, + { + "code": "GJ", + "name": "Gorj" + }, + { + "code": "HR", + "name": "Harghita" + }, + { + "code": "HD", + "name": "Hunedoara" + }, + { + "code": "IL", + "name": "Ialomița" + }, + { + "code": "IS", + "name": "Iași" + }, + { + "code": "IF", + "name": "Ilfov" + }, + { + "code": "MM", + "name": "Maramureș" + }, + { + "code": "MH", + "name": "Mehedinți" + }, + { + "code": "MS", + "name": "Mureș" + }, + { + "code": "NT", + "name": "Neamț" + }, + { + "code": "OT", + "name": "Olt" + }, + { + "code": "PH", + "name": "Prahova" + }, + { + "code": "SJ", + "name": "Sălaj" + }, + { + "code": "SM", + "name": "Satu Mare" + }, + { + "code": "SB", + "name": "Sibiu" + }, + { + "code": "SV", + "name": "Suceava" + }, + { + "code": "TR", + "name": "Teleorman" + }, + { + "code": "TM", + "name": "Timiș" + }, + { + "code": "TL", + "name": "Tulcea" + }, + { + "code": "VL", + "name": "Vâlcea" + }, + { + "code": "VS", + "name": "Vaslui" + }, + { + "code": "VN", + "name": "Vrancea" + } + ] + }, + { + "code": "RS", + "name": "Serbian dinar", + "currency_code": "RSD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "RS00", + "name": "Belgrade" + }, + { + "code": "RS14", + "name": "Bor" + }, + { + "code": "RS11", + "name": "Braničevo" + }, + { + "code": "RS02", + "name": "Central Banat" + }, + { + "code": "RS10", + "name": "Danube" + }, + { + "code": "RS23", + "name": "Jablanica" + }, + { + "code": "RS09", + "name": "Kolubara" + }, + { + "code": "RS08", + "name": "Mačva" + }, + { + "code": "RS17", + "name": "Morava" + }, + { + "code": "RS20", + "name": "Nišava" + }, + { + "code": "RS01", + "name": "North Bačka" + }, + { + "code": "RS03", + "name": "North Banat" + }, + { + "code": "RS24", + "name": "Pčinja" + }, + { + "code": "RS22", + "name": "Pirot" + }, + { + "code": "RS13", + "name": "Pomoravlje" + }, + { + "code": "RS19", + "name": "Rasina" + }, + { + "code": "RS18", + "name": "Raška" + }, + { + "code": "RS06", + "name": "South Bačka" + }, + { + "code": "RS04", + "name": "South Banat" + }, + { + "code": "RS07", + "name": "Srem" + }, + { + "code": "RS12", + "name": "Šumadija" + }, + { + "code": "RS21", + "name": "Toplica" + }, + { + "code": "RS05", + "name": "West Bačka" + }, + { + "code": "RS15", + "name": "Zaječar" + }, + { + "code": "RS16", + "name": "Zlatibor" + }, + { + "code": "RS25", + "name": "Kosovo" + }, + { + "code": "RS26", + "name": "Peć" + }, + { + "code": "RS27", + "name": "Prizren" + }, + { + "code": "RS28", + "name": "Kosovska Mitrovica" + }, + { + "code": "RS29", + "name": "Kosovo-Pomoravlje" + }, + { + "code": "RSKM", + "name": "Kosovo-Metohija" + }, + { + "code": "RSVO", + "name": "Vojvodina" + } + ] + }, + { + "code": "RU", + "name": "Russian ruble", + "currency_code": "RUB", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SE", + "name": "Swedish krona", + "currency_code": "SEK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SJ", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SK", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SM", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TR", + "name": "Turkish lira", + "currency_code": "TRY", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "TR01", + "name": "Adana" + }, + { + "code": "TR02", + "name": "Adıyaman" + }, + { + "code": "TR03", + "name": "Afyon" + }, + { + "code": "TR04", + "name": "Ağrı" + }, + { + "code": "TR05", + "name": "Amasya" + }, + { + "code": "TR06", + "name": "Ankara" + }, + { + "code": "TR07", + "name": "Antalya" + }, + { + "code": "TR08", + "name": "Artvin" + }, + { + "code": "TR09", + "name": "Aydın" + }, + { + "code": "TR10", + "name": "Balıkesir" + }, + { + "code": "TR11", + "name": "Bilecik" + }, + { + "code": "TR12", + "name": "Bingöl" + }, + { + "code": "TR13", + "name": "Bitlis" + }, + { + "code": "TR14", + "name": "Bolu" + }, + { + "code": "TR15", + "name": "Burdur" + }, + { + "code": "TR16", + "name": "Bursa" + }, + { + "code": "TR17", + "name": "Çanakkale" + }, + { + "code": "TR18", + "name": "Çankırı" + }, + { + "code": "TR19", + "name": "Çorum" + }, + { + "code": "TR20", + "name": "Denizli" + }, + { + "code": "TR21", + "name": "Diyarbakır" + }, + { + "code": "TR22", + "name": "Edirne" + }, + { + "code": "TR23", + "name": "Elazığ" + }, + { + "code": "TR24", + "name": "Erzincan" + }, + { + "code": "TR25", + "name": "Erzurum" + }, + { + "code": "TR26", + "name": "Eskişehir" + }, + { + "code": "TR27", + "name": "Gaziantep" + }, + { + "code": "TR28", + "name": "Giresun" + }, + { + "code": "TR29", + "name": "Gümüşhane" + }, + { + "code": "TR30", + "name": "Hakkari" + }, + { + "code": "TR31", + "name": "Hatay" + }, + { + "code": "TR32", + "name": "Isparta" + }, + { + "code": "TR33", + "name": "İçel" + }, + { + "code": "TR34", + "name": "İstanbul" + }, + { + "code": "TR35", + "name": "İzmir" + }, + { + "code": "TR36", + "name": "Kars" + }, + { + "code": "TR37", + "name": "Kastamonu" + }, + { + "code": "TR38", + "name": "Kayseri" + }, + { + "code": "TR39", + "name": "Kırklareli" + }, + { + "code": "TR40", + "name": "Kırşehir" + }, + { + "code": "TR41", + "name": "Kocaeli" + }, + { + "code": "TR42", + "name": "Konya" + }, + { + "code": "TR43", + "name": "Kütahya" + }, + { + "code": "TR44", + "name": "Malatya" + }, + { + "code": "TR45", + "name": "Manisa" + }, + { + "code": "TR46", + "name": "Kahramanmaraş" + }, + { + "code": "TR47", + "name": "Mardin" + }, + { + "code": "TR48", + "name": "Muğla" + }, + { + "code": "TR49", + "name": "Muş" + }, + { + "code": "TR50", + "name": "Nevşehir" + }, + { + "code": "TR51", + "name": "Niğde" + }, + { + "code": "TR52", + "name": "Ordu" + }, + { + "code": "TR53", + "name": "Rize" + }, + { + "code": "TR54", + "name": "Sakarya" + }, + { + "code": "TR55", + "name": "Samsun" + }, + { + "code": "TR56", + "name": "Siirt" + }, + { + "code": "TR57", + "name": "Sinop" + }, + { + "code": "TR58", + "name": "Sivas" + }, + { + "code": "TR59", + "name": "Tekirdağ" + }, + { + "code": "TR60", + "name": "Tokat" + }, + { + "code": "TR61", + "name": "Trabzon" + }, + { + "code": "TR62", + "name": "Tunceli" + }, + { + "code": "TR63", + "name": "Şanlıurfa" + }, + { + "code": "TR64", + "name": "Uşak" + }, + { + "code": "TR65", + "name": "Van" + }, + { + "code": "TR66", + "name": "Yozgat" + }, + { + "code": "TR67", + "name": "Zonguldak" + }, + { + "code": "TR68", + "name": "Aksaray" + }, + { + "code": "TR69", + "name": "Bayburt" + }, + { + "code": "TR70", + "name": "Karaman" + }, + { + "code": "TR71", + "name": "Kırıkkale" + }, + { + "code": "TR72", + "name": "Batman" + }, + { + "code": "TR73", + "name": "Şırnak" + }, + { + "code": "TR74", + "name": "Bartın" + }, + { + "code": "TR75", + "name": "Ardahan" + }, + { + "code": "TR76", + "name": "Iğdır" + }, + { + "code": "TR77", + "name": "Yalova" + }, + { + "code": "TR78", + "name": "Karabük" + }, + { + "code": "TR79", + "name": "Kilis" + }, + { + "code": "TR80", + "name": "Osmaniye" + }, + { + "code": "TR81", + "name": "Düzce" + } + ] + }, + { + "code": "UA", + "name": "Ukrainian hryvnia", + "currency_code": "UAH", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "VN", + "name": "Vinnytsia Oblast" + }, + { + "code": "VL", + "name": "Volyn Oblast" + }, + { + "code": "DP", + "name": "Dnipropetrovsk Oblast" + }, + { + "code": "DT", + "name": "Donetsk Oblast" + }, + { + "code": "ZT", + "name": "Zhytomyr Oblast" + }, + { + "code": "ZK", + "name": "Zakarpattia Oblast" + }, + { + "code": "ZP", + "name": "Zaporizhzhia Oblast" + }, + { + "code": "IF", + "name": "Ivano-Frankivsk Oblast" + }, + { + "code": "KV", + "name": "Kyiv Oblast" + }, + { + "code": "KH", + "name": "Kirovohrad Oblast" + }, + { + "code": "LH", + "name": "Luhansk Oblast" + }, + { + "code": "LV", + "name": "Lviv Oblast" + }, + { + "code": "MY", + "name": "Mykolaiv Oblast" + }, + { + "code": "OD", + "name": "Odessa Oblast" + }, + { + "code": "PL", + "name": "Poltava Oblast" + }, + { + "code": "RV", + "name": "Rivne Oblast" + }, + { + "code": "SM", + "name": "Sumy Oblast" + }, + { + "code": "TP", + "name": "Ternopil Oblast" + }, + { + "code": "KK", + "name": "Kharkiv Oblast" + }, + { + "code": "KS", + "name": "Kherson Oblast" + }, + { + "code": "KM", + "name": "Khmelnytskyi Oblast" + }, + { + "code": "CK", + "name": "Cherkasy Oblast" + }, + { + "code": "CH", + "name": "Chernihiv Oblast" + }, + { + "code": "CV", + "name": "Chernivtsi Oblast" + } + ] + }, + { + "code": "VA", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "NA", + "name": "North America", + "countries": [{ + "code": "AG", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AI", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AW", + "name": "Aruban florin", + "currency_code": "AWG", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BB", + "name": "Barbadian dollar", + "currency_code": "BBD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BL", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BM", + "name": "Bermudian dollar", + "currency_code": "BMD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BQ", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BS", + "name": "Bahamian dollar", + "currency_code": "BSD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BZ", + "name": "Belize dollar", + "currency_code": "BZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CA", + "name": "Canadian dollar", + "currency_code": "CAD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Alberta" + }, + { + "code": "BC", + "name": "British Columbia" + }, + { + "code": "MB", + "name": "Manitoba" + }, + { + "code": "NB", + "name": "New Brunswick" + }, + { + "code": "NL", + "name": "Newfoundland and Labrador" + }, + { + "code": "NT", + "name": "Northwest Territories" + }, + { + "code": "NS", + "name": "Nova Scotia" + }, + { + "code": "NU", + "name": "Nunavut" + }, + { + "code": "ON", + "name": "Ontario" + }, + { + "code": "PE", + "name": "Prince Edward Island" + }, + { + "code": "QC", + "name": "Quebec" + }, + { + "code": "SK", + "name": "Saskatchewan" + }, + { + "code": "YT", + "name": "Yukon Territory" + } + ] + }, + { + "code": "CR", + "name": "Costa Rican colón", + "currency_code": "CRC", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "CR-A", + "name": "Alajuela" + }, + { + "code": "CR-C", + "name": "Cartago" + }, + { + "code": "CR-G", + "name": "Guanacaste" + }, + { + "code": "CR-H", + "name": "Heredia" + }, + { + "code": "CR-L", + "name": "Limón" + }, + { + "code": "CR-P", + "name": "Puntarenas" + }, + { + "code": "CR-SJ", + "name": "San José" + } + ] + }, + { + "code": "CU", + "name": "Cuban convertible peso", + "currency_code": "CUC", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CW", + "name": "Netherlands Antillean guilder", + "currency_code": "ANG", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DM", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DO", + "name": "Dominican peso", + "currency_code": "DOP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "DO-01", + "name": "Distrito Nacional" + }, + { + "code": "DO-02", + "name": "Azua" + }, + { + "code": "DO-03", + "name": "Baoruco" + }, + { + "code": "DO-04", + "name": "Barahona" + }, + { + "code": "DO-33", + "name": "Cibao Nordeste" + }, + { + "code": "DO-34", + "name": "Cibao Noroeste" + }, + { + "code": "DO-35", + "name": "Cibao Norte" + }, + { + "code": "DO-36", + "name": "Cibao Sur" + }, + { + "code": "DO-05", + "name": "Dajabón" + }, + { + "code": "DO-06", + "name": "Duarte" + }, + { + "code": "DO-08", + "name": "El Seibo" + }, + { + "code": "DO-37", + "name": "El Valle" + }, + { + "code": "DO-07", + "name": "Elías Piña" + }, + { + "code": "DO-38", + "name": "Enriquillo" + }, + { + "code": "DO-09", + "name": "Espaillat" + }, + { + "code": "DO-30", + "name": "Hato Mayor" + }, + { + "code": "DO-19", + "name": "Hermanas Mirabal" + }, + { + "code": "DO-39", + "name": "Higüamo" + }, + { + "code": "DO-10", + "name": "Independencia" + }, + { + "code": "DO-11", + "name": "La Altagracia" + }, + { + "code": "DO-12", + "name": "La Romana" + }, + { + "code": "DO-13", + "name": "La Vega" + }, + { + "code": "DO-14", + "name": "María Trinidad Sánchez" + }, + { + "code": "DO-28", + "name": "Monseñor Nouel" + }, + { + "code": "DO-15", + "name": "Monte Cristi" + }, + { + "code": "DO-29", + "name": "Monte Plata" + }, + { + "code": "DO-40", + "name": "Ozama" + }, + { + "code": "DO-16", + "name": "Pedernales" + }, + { + "code": "DO-17", + "name": "Peravia" + }, + { + "code": "DO-18", + "name": "Puerto Plata" + }, + { + "code": "DO-20", + "name": "Samaná" + }, + { + "code": "DO-21", + "name": "San Cristóbal" + }, + { + "code": "DO-31", + "name": "San José de Ocoa" + }, + { + "code": "DO-22", + "name": "San Juan" + }, + { + "code": "DO-23", + "name": "San Pedro de Macorís" + }, + { + "code": "DO-24", + "name": "Sánchez Ramírez" + }, + { + "code": "DO-25", + "name": "Santiago" + }, + { + "code": "DO-26", + "name": "Santiago Rodríguez" + }, + { + "code": "DO-32", + "name": "Santo Domingo" + }, + { + "code": "DO-41", + "name": "Valdesia" + }, + { + "code": "DO-27", + "name": "Valverde" + }, + { + "code": "DO-42", + "name": "Yuma" + } + ] + }, + { + "code": "GD", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GL", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GP", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GT", + "name": "Guatemalan quetzal", + "currency_code": "GTQ", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "GT-AV", + "name": "Alta Verapaz" + }, + { + "code": "GT-BV", + "name": "Baja Verapaz" + }, + { + "code": "GT-CM", + "name": "Chimaltenango" + }, + { + "code": "GT-CQ", + "name": "Chiquimula" + }, + { + "code": "GT-PR", + "name": "El Progreso" + }, + { + "code": "GT-ES", + "name": "Escuintla" + }, + { + "code": "GT-GU", + "name": "Guatemala" + }, + { + "code": "GT-HU", + "name": "Huehuetenango" + }, + { + "code": "GT-IZ", + "name": "Izabal" + }, + { + "code": "GT-JA", + "name": "Jalapa" + }, + { + "code": "GT-JU", + "name": "Jutiapa" + }, + { + "code": "GT-PE", + "name": "Petén" + }, + { + "code": "GT-QZ", + "name": "Quetzaltenango" + }, + { + "code": "GT-QC", + "name": "Quiché" + }, + { + "code": "GT-RE", + "name": "Retalhuleu" + }, + { + "code": "GT-SA", + "name": "Sacatepéquez" + }, + { + "code": "GT-SM", + "name": "San Marcos" + }, + { + "code": "GT-SR", + "name": "Santa Rosa" + }, + { + "code": "GT-SO", + "name": "Sololá" + }, + { + "code": "GT-SU", + "name": "Suchitepéquez" + }, + { + "code": "GT-TO", + "name": "Totonicapán" + }, + { + "code": "GT-ZA", + "name": "Zacapa" + } + ] + }, + { + "code": "HN", + "name": "Honduran lempira", + "currency_code": "HNL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "HN-AT", + "name": "Atlántida" + }, + { + "code": "HN-IB", + "name": "Bay Islands" + }, + { + "code": "HN-CH", + "name": "Choluteca" + }, + { + "code": "HN-CL", + "name": "Colón" + }, + { + "code": "HN-CM", + "name": "Comayagua" + }, + { + "code": "HN-CP", + "name": "Copán" + }, + { + "code": "HN-CR", + "name": "Cortés" + }, + { + "code": "HN-EP", + "name": "El Paraíso" + }, + { + "code": "HN-FM", + "name": "Francisco Morazán" + }, + { + "code": "HN-GD", + "name": "Gracias a Dios" + }, + { + "code": "HN-IN", + "name": "Intibucá" + }, + { + "code": "HN-LE", + "name": "Lempira" + }, + { + "code": "HN-LP", + "name": "La Paz" + }, + { + "code": "HN-OC", + "name": "Ocotepeque" + }, + { + "code": "HN-OL", + "name": "Olancho" + }, + { + "code": "HN-SB", + "name": "Santa Bárbara" + }, + { + "code": "HN-VA", + "name": "Valle" + }, + { + "code": "HN-YO", + "name": "Yoro" + } + ] + }, + { + "code": "HT", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "JM", + "name": "Jamaican dollar", + "currency_code": "JMD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JM-01", + "name": "Kingston" + }, + { + "code": "JM-02", + "name": "Saint Andrew" + }, + { + "code": "JM-03", + "name": "Saint Thomas" + }, + { + "code": "JM-04", + "name": "Portland" + }, + { + "code": "JM-05", + "name": "Saint Mary" + }, + { + "code": "JM-06", + "name": "Saint Ann" + }, + { + "code": "JM-07", + "name": "Trelawny" + }, + { + "code": "JM-08", + "name": "Saint James" + }, + { + "code": "JM-09", + "name": "Hanover" + }, + { + "code": "JM-10", + "name": "Westmoreland" + }, + { + "code": "JM-11", + "name": "Saint Elizabeth" + }, + { + "code": "JM-12", + "name": "Manchester" + }, + { + "code": "JM-13", + "name": "Clarendon" + }, + { + "code": "JM-14", + "name": "Saint Catherine" + } + ] + }, + { + "code": "KN", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KY", + "name": "Cayman Islands dollar", + "currency_code": "KYD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LC", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MF", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MQ", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MS", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MX", + "name": "Mexican peso", + "currency_code": "MXN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "DF", + "name": "Ciudad de México" + }, + { + "code": "JA", + "name": "Jalisco" + }, + { + "code": "NL", + "name": "Nuevo León" + }, + { + "code": "AG", + "name": "Aguascalientes" + }, + { + "code": "BC", + "name": "Baja California" + }, + { + "code": "BS", + "name": "Baja California Sur" + }, + { + "code": "CM", + "name": "Campeche" + }, + { + "code": "CS", + "name": "Chiapas" + }, + { + "code": "CH", + "name": "Chihuahua" + }, + { + "code": "CO", + "name": "Coahuila" + }, + { + "code": "CL", + "name": "Colima" + }, + { + "code": "DG", + "name": "Durango" + }, + { + "code": "GT", + "name": "Guanajuato" + }, + { + "code": "GR", + "name": "Guerrero" + }, + { + "code": "HG", + "name": "Hidalgo" + }, + { + "code": "MX", + "name": "Estado de México" + }, + { + "code": "MI", + "name": "Michoacán" + }, + { + "code": "MO", + "name": "Morelos" + }, + { + "code": "NA", + "name": "Nayarit" + }, + { + "code": "OA", + "name": "Oaxaca" + }, + { + "code": "PU", + "name": "Puebla" + }, + { + "code": "QT", + "name": "Querétaro" + }, + { + "code": "QR", + "name": "Quintana Roo" + }, + { + "code": "SL", + "name": "San Luis Potosí" + }, + { + "code": "SI", + "name": "Sinaloa" + }, + { + "code": "SO", + "name": "Sonora" + }, + { + "code": "TB", + "name": "Tabasco" + }, + { + "code": "TM", + "name": "Tamaulipas" + }, + { + "code": "TL", + "name": "Tlaxcala" + }, + { + "code": "VE", + "name": "Veracruz" + }, + { + "code": "YU", + "name": "Yucatán" + }, + { + "code": "ZA", + "name": "Zacatecas" + } + ] + }, + { + "code": "NI", + "name": "Nicaraguan córdoba", + "currency_code": "NIO", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "NI-AN", + "name": "Atlántico Norte" + }, + { + "code": "NI-AS", + "name": "Atlántico Sur" + }, + { + "code": "NI-BO", + "name": "Boaco" + }, + { + "code": "NI-CA", + "name": "Carazo" + }, + { + "code": "NI-CI", + "name": "Chinandega" + }, + { + "code": "NI-CO", + "name": "Chontales" + }, + { + "code": "NI-ES", + "name": "Estelí" + }, + { + "code": "NI-GR", + "name": "Granada" + }, + { + "code": "NI-JI", + "name": "Jinotega" + }, + { + "code": "NI-LE", + "name": "León" + }, + { + "code": "NI-MD", + "name": "Madriz" + }, + { + "code": "NI-MN", + "name": "Managua" + }, + { + "code": "NI-MS", + "name": "Masaya" + }, + { + "code": "NI-MT", + "name": "Matagalpa" + }, + { + "code": "NI-NS", + "name": "Nueva Segovia" + }, + { + "code": "NI-RI", + "name": "Rivas" + }, + { + "code": "NI-SJ", + "name": "Río San Juan" + } + ] + }, + { + "code": "PA", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "PA-1", + "name": "Bocas del Toro" + }, + { + "code": "PA-2", + "name": "Coclé" + }, + { + "code": "PA-3", + "name": "Colón" + }, + { + "code": "PA-4", + "name": "Chiriquí" + }, + { + "code": "PA-5", + "name": "Darién" + }, + { + "code": "PA-6", + "name": "Herrera" + }, + { + "code": "PA-7", + "name": "Los Santos" + }, + { + "code": "PA-8", + "name": "Panamá" + }, + { + "code": "PA-9", + "name": "Veraguas" + }, + { + "code": "PA-10", + "name": "West Panamá" + }, + { + "code": "PA-EM", + "name": "Emberá" + }, + { + "code": "PA-KY", + "name": "Guna Yala" + }, + { + "code": "PA-NB", + "name": "Ngöbe-Buglé" + } + ] + }, + { + "code": "PM", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PR", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SV", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "SV-AH", + "name": "Ahuachapán" + }, + { + "code": "SV-CA", + "name": "Cabañas" + }, + { + "code": "SV-CH", + "name": "Chalatenango" + }, + { + "code": "SV-CU", + "name": "Cuscatlán" + }, + { + "code": "SV-LI", + "name": "La Libertad" + }, + { + "code": "SV-MO", + "name": "Morazán" + }, + { + "code": "SV-PA", + "name": "La Paz" + }, + { + "code": "SV-SA", + "name": "Santa Ana" + }, + { + "code": "SV-SM", + "name": "San Miguel" + }, + { + "code": "SV-SO", + "name": "Sonsonate" + }, + { + "code": "SV-SS", + "name": "San Salvador" + }, + { + "code": "SV-SV", + "name": "San Vicente" + }, + { + "code": "SV-UN", + "name": "La Unión" + }, + { + "code": "SV-US", + "name": "Usulután" + } + ] + }, + { + "code": "SX", + "name": "Netherlands Antillean guilder", + "currency_code": "ANG", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TC", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TT", + "name": "Trinidad and Tobago dollar", + "currency_code": "TTD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "US", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "foot", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "oz", + "states": [{ + "code": "AL", + "name": "Alabama" + }, + { + "code": "AK", + "name": "Alaska" + }, + { + "code": "AZ", + "name": "Arizona" + }, + { + "code": "AR", + "name": "Arkansas" + }, + { + "code": "CA", + "name": "California" + }, + { + "code": "CO", + "name": "Colorado" + }, + { + "code": "CT", + "name": "Connecticut" + }, + { + "code": "DE", + "name": "Delaware" + }, + { + "code": "DC", + "name": "District Of Columbia" + }, + { + "code": "FL", + "name": "Florida" + }, + { + "code": "GA", + "name": "Georgia" + }, + { + "code": "HI", + "name": "Hawaii" + }, + { + "code": "ID", + "name": "Idaho" + }, + { + "code": "IL", + "name": "Illinois" + }, + { + "code": "IN", + "name": "Indiana" + }, + { + "code": "IA", + "name": "Iowa" + }, + { + "code": "KS", + "name": "Kansas" + }, + { + "code": "KY", + "name": "Kentucky" + }, + { + "code": "LA", + "name": "Louisiana" + }, + { + "code": "ME", + "name": "Maine" + }, + { + "code": "MD", + "name": "Maryland" + }, + { + "code": "MA", + "name": "Massachusetts" + }, + { + "code": "MI", + "name": "Michigan" + }, + { + "code": "MN", + "name": "Minnesota" + }, + { + "code": "MS", + "name": "Mississippi" + }, + { + "code": "MO", + "name": "Missouri" + }, + { + "code": "MT", + "name": "Montana" + }, + { + "code": "NE", + "name": "Nebraska" + }, + { + "code": "NV", + "name": "Nevada" + }, + { + "code": "NH", + "name": "New Hampshire" + }, + { + "code": "NJ", + "name": "New Jersey" + }, + { + "code": "NM", + "name": "New Mexico" + }, + { + "code": "NY", + "name": "New York" + }, + { + "code": "NC", + "name": "North Carolina" + }, + { + "code": "ND", + "name": "North Dakota" + }, + { + "code": "OH", + "name": "Ohio" + }, + { + "code": "OK", + "name": "Oklahoma" + }, + { + "code": "OR", + "name": "Oregon" + }, + { + "code": "PA", + "name": "Pennsylvania" + }, + { + "code": "RI", + "name": "Rhode Island" + }, + { + "code": "SC", + "name": "South Carolina" + }, + { + "code": "SD", + "name": "South Dakota" + }, + { + "code": "TN", + "name": "Tennessee" + }, + { + "code": "TX", + "name": "Texas" + }, + { + "code": "UT", + "name": "Utah" + }, + { + "code": "VT", + "name": "Vermont" + }, + { + "code": "VA", + "name": "Virginia" + }, + { + "code": "WA", + "name": "Washington" + }, + { + "code": "WV", + "name": "West Virginia" + }, + { + "code": "WI", + "name": "Wisconsin" + }, + { + "code": "WY", + "name": "Wyoming" + }, + { + "code": "AA", + "name": "Armed Forces (AA)" + }, + { + "code": "AE", + "name": "Armed Forces (AE)" + }, + { + "code": "AP", + "name": "Armed Forces (AP)" + } + ] + }, + { + "code": "VC", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "VG", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "VI", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "OC", + "name": "Oceania", + "countries": [{ + "code": "AS", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AU", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "code": "NSW", + "name": "New South Wales" + }, + { + "code": "NT", + "name": "Northern Territory" + }, + { + "code": "QLD", + "name": "Queensland" + }, + { + "code": "SA", + "name": "South Australia" + }, + { + "code": "TAS", + "name": "Tasmania" + }, + { + "code": "VIC", + "name": "Victoria" + }, + { + "code": "WA", + "name": "Western Australia" + } + ] + }, + { + "code": "CK", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FJ", + "name": "Fijian dollar", + "currency_code": "FJD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FM", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GU", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KI", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MH", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MP", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NC", + "name": "CFP franc", + "currency_code": "XPF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NF", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NR", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NU", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NZ", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "NTL", + "name": "Northland" + }, + { + "code": "AUK", + "name": "Auckland" + }, + { + "code": "WKO", + "name": "Waikato" + }, + { + "code": "BOP", + "name": "Bay of Plenty" + }, + { + "code": "TKI", + "name": "Taranaki" + }, + { + "code": "GIS", + "name": "Gisborne" + }, + { + "code": "HKB", + "name": "Hawke’s Bay" + }, + { + "code": "MWT", + "name": "Manawatu-Wanganui" + }, + { + "code": "WGN", + "name": "Wellington" + }, + { + "code": "NSN", + "name": "Nelson" + }, + { + "code": "MBH", + "name": "Marlborough" + }, + { + "code": "TAS", + "name": "Tasman" + }, + { + "code": "WTC", + "name": "West Coast" + }, + { + "code": "CAN", + "name": "Canterbury" + }, + { + "code": "OTA", + "name": "Otago" + }, + { + "code": "STL", + "name": "Southland" + } + ] + }, + { + "code": "PF", + "name": "CFP franc", + "currency_code": "XPF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PG", + "name": "Papua New Guinean kina", + "currency_code": "PGK", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PN", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PW", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SB", + "name": "Solomon Islands dollar", + "currency_code": "SBD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TK", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TO", + "name": "Tongan paʻanga", + "currency_code": "TOP", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TV", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "UM", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": 81, + "name": "Baker Island" + }, + { + "code": 84, + "name": "Howland Island" + }, + { + "code": 86, + "name": "Jarvis Island" + }, + { + "code": 67, + "name": "Johnston Atoll" + }, + { + "code": 89, + "name": "Kingman Reef" + }, + { + "code": 71, + "name": "Midway Atoll" + }, + { + "code": 76, + "name": "Navassa Island" + }, + { + "code": 95, + "name": "Palmyra Atoll" + }, + { + "code": 79, + "name": "Wake Island" + } + ] + }, + { + "code": "VU", + "name": "Vanuatu vatu", + "currency_code": "VUV", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "WF", + "name": "CFP franc", + "currency_code": "XPF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "WS", + "name": "Samoan tālā", + "currency_code": "WST", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "SA", + "name": "South America", + "countries": [{ + "code": "AR", + "name": "Argentine peso", + "currency_code": "ARS", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "Ciudad Autónoma de Buenos Aires" + }, + { + "code": "B", + "name": "Buenos Aires" + }, + { + "code": "K", + "name": "Catamarca" + }, + { + "code": "H", + "name": "Chaco" + }, + { + "code": "U", + "name": "Chubut" + }, + { + "code": "X", + "name": "Córdoba" + }, + { + "code": "W", + "name": "Corrientes" + }, + { + "code": "E", + "name": "Entre Ríos" + }, + { + "code": "P", + "name": "Formosa" + }, + { + "code": "Y", + "name": "Jujuy" + }, + { + "code": "L", + "name": "La Pampa" + }, + { + "code": "F", + "name": "La Rioja" + }, + { + "code": "M", + "name": "Mendoza" + }, + { + "code": "N", + "name": "Misiones" + }, + { + "code": "Q", + "name": "Neuquén" + }, + { + "code": "R", + "name": "Río Negro" + }, + { + "code": "A", + "name": "Salta" + }, + { + "code": "J", + "name": "San Juan" + }, + { + "code": "D", + "name": "San Luis" + }, + { + "code": "Z", + "name": "Santa Cruz" + }, + { + "code": "S", + "name": "Santa Fe" + }, + { + "code": "G", + "name": "Santiago del Estero" + }, + { + "code": "V", + "name": "Tierra del Fuego" + }, + { + "code": "T", + "name": "Tucumán" + } + ] + }, + { + "code": "BO", + "name": "Bolivian boliviano", + "currency_code": "BOB", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "BO-B", + "name": "Beni" + }, + { + "code": "BO-H", + "name": "Chuquisaca" + }, + { + "code": "BO-C", + "name": "Cochabamba" + }, + { + "code": "BO-L", + "name": "La Paz" + }, + { + "code": "BO-O", + "name": "Oruro" + }, + { + "code": "BO-N", + "name": "Pando" + }, + { + "code": "BO-P", + "name": "Potosí" + }, + { + "code": "BO-S", + "name": "Santa Cruz" + }, + { + "code": "BO-T", + "name": "Tarija" + } + ] + }, + { + "code": "BR", + "name": "Brazilian real", + "currency_code": "BRL", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AC", + "name": "Acre" + }, + { + "code": "AL", + "name": "Alagoas" + }, + { + "code": "AP", + "name": "Amapá" + }, + { + "code": "AM", + "name": "Amazonas" + }, + { + "code": "BA", + "name": "Bahia" + }, + { + "code": "CE", + "name": "Ceará" + }, + { + "code": "DF", + "name": "Distrito Federal" + }, + { + "code": "ES", + "name": "Espírito Santo" + }, + { + "code": "GO", + "name": "Goiás" + }, + { + "code": "MA", + "name": "Maranhão" + }, + { + "code": "MT", + "name": "Mato Grosso" + }, + { + "code": "MS", + "name": "Mato Grosso do Sul" + }, + { + "code": "MG", + "name": "Minas Gerais" + }, + { + "code": "PA", + "name": "Pará" + }, + { + "code": "PB", + "name": "Paraíba" + }, + { + "code": "PR", + "name": "Paraná" + }, + { + "code": "PE", + "name": "Pernambuco" + }, + { + "code": "PI", + "name": "Piauí" + }, + { + "code": "RJ", + "name": "Rio de Janeiro" + }, + { + "code": "RN", + "name": "Rio Grande do Norte" + }, + { + "code": "RS", + "name": "Rio Grande do Sul" + }, + { + "code": "RO", + "name": "Rondônia" + }, + { + "code": "RR", + "name": "Roraima" + }, + { + "code": "SC", + "name": "Santa Catarina" + }, + { + "code": "SP", + "name": "São Paulo" + }, + { + "code": "SE", + "name": "Sergipe" + }, + { + "code": "TO", + "name": "Tocantins" + } + ] + }, + { + "code": "CL", + "name": "Chilean peso", + "currency_code": "CLP", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "CL-AI", + "name": "Aisén del General Carlos Ibañez del Campo" + }, + { + "code": "CL-AN", + "name": "Antofagasta" + }, + { + "code": "CL-AP", + "name": "Arica y Parinacota" + }, + { + "code": "CL-AR", + "name": "La Araucanía" + }, + { + "code": "CL-AT", + "name": "Atacama" + }, + { + "code": "CL-BI", + "name": "Biobío" + }, + { + "code": "CL-CO", + "name": "Coquimbo" + }, + { + "code": "CL-LI", + "name": "Libertador General Bernardo O'Higgins" + }, + { + "code": "CL-LL", + "name": "Los Lagos" + }, + { + "code": "CL-LR", + "name": "Los Ríos" + }, + { + "code": "CL-MA", + "name": "Magallanes" + }, + { + "code": "CL-ML", + "name": "Maule" + }, + { + "code": "CL-NB", + "name": "Ñuble" + }, + { + "code": "CL-RM", + "name": "Región Metropolitana de Santiago" + }, + { + "code": "CL-TA", + "name": "Tarapacá" + }, + { + "code": "CL-VS", + "name": "Valparaíso" + } + ] + }, + { + "code": "CO", + "name": "Colombian peso", + "currency_code": "COP", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "CO-AMA", + "name": "Amazonas" + }, + { + "code": "CO-ANT", + "name": "Antioquia" + }, + { + "code": "CO-ARA", + "name": "Arauca" + }, + { + "code": "CO-ATL", + "name": "Atlántico" + }, + { + "code": "CO-BOL", + "name": "Bolívar" + }, + { + "code": "CO-BOY", + "name": "Boyacá" + }, + { + "code": "CO-CAL", + "name": "Caldas" + }, + { + "code": "CO-CAQ", + "name": "Caquetá" + }, + { + "code": "CO-CAS", + "name": "Casanare" + }, + { + "code": "CO-CAU", + "name": "Cauca" + }, + { + "code": "CO-CES", + "name": "Cesar" + }, + { + "code": "CO-CHO", + "name": "Chocó" + }, + { + "code": "CO-COR", + "name": "Córdoba" + }, + { + "code": "CO-CUN", + "name": "Cundinamarca" + }, + { + "code": "CO-DC", + "name": "Capital District" + }, + { + "code": "CO-GUA", + "name": "Guainía" + }, + { + "code": "CO-GUV", + "name": "Guaviare" + }, + { + "code": "CO-HUI", + "name": "Huila" + }, + { + "code": "CO-LAG", + "name": "La Guajira" + }, + { + "code": "CO-MAG", + "name": "Magdalena" + }, + { + "code": "CO-MET", + "name": "Meta" + }, + { + "code": "CO-NAR", + "name": "Nariño" + }, + { + "code": "CO-NSA", + "name": "Norte de Santander" + }, + { + "code": "CO-PUT", + "name": "Putumayo" + }, + { + "code": "CO-QUI", + "name": "Quindío" + }, + { + "code": "CO-RIS", + "name": "Risaralda" + }, + { + "code": "CO-SAN", + "name": "Santander" + }, + { + "code": "CO-SAP", + "name": "San Andrés & Providencia" + }, + { + "code": "CO-SUC", + "name": "Sucre" + }, + { + "code": "CO-TOL", + "name": "Tolima" + }, + { + "code": "CO-VAC", + "name": "Valle del Cauca" + }, + { + "code": "CO-VAU", + "name": "Vaupés" + }, + { + "code": "CO-VID", + "name": "Vichada" + } + ] + }, + { + "code": "EC", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "EC-A", + "name": "Azuay" + }, + { + "code": "EC-B", + "name": "Bolívar" + }, + { + "code": "EC-F", + "name": "Cañar" + }, + { + "code": "EC-C", + "name": "Carchi" + }, + { + "code": "EC-H", + "name": "Chimborazo" + }, + { + "code": "EC-X", + "name": "Cotopaxi" + }, + { + "code": "EC-O", + "name": "El Oro" + }, + { + "code": "EC-E", + "name": "Esmeraldas" + }, + { + "code": "EC-W", + "name": "Galápagos" + }, + { + "code": "EC-G", + "name": "Guayas" + }, + { + "code": "EC-I", + "name": "Imbabura" + }, + { + "code": "EC-L", + "name": "Loja" + }, + { + "code": "EC-R", + "name": "Los Ríos" + }, + { + "code": "EC-M", + "name": "Manabí" + }, + { + "code": "EC-S", + "name": "Morona-Santiago" + }, + { + "code": "EC-N", + "name": "Napo" + }, + { + "code": "EC-D", + "name": "Orellana" + }, + { + "code": "EC-Y", + "name": "Pastaza" + }, + { + "code": "EC-P", + "name": "Pichincha" + }, + { + "code": "EC-SE", + "name": "Santa Elena" + }, + { + "code": "EC-SD", + "name": "Santo Domingo de los Tsáchilas" + }, + { + "code": "EC-U", + "name": "Sucumbíos" + }, + { + "code": "EC-T", + "name": "Tungurahua" + }, + { + "code": "EC-Z", + "name": "Zamora-Chinchipe" + } + ] + }, + { + "code": "FK", + "name": "Falkland Islands pound", + "currency_code": "FKP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GF", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GY", + "name": "Guyanese dollar", + "currency_code": "GYD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PE", + "name": "Sol", + "currency_code": "PEN", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CAL", + "name": "El Callao" + }, + { + "code": "LMA", + "name": "Municipalidad Metropolitana de Lima" + }, + { + "code": "AMA", + "name": "Amazonas" + }, + { + "code": "ANC", + "name": "Ancash" + }, + { + "code": "APU", + "name": "Apurímac" + }, + { + "code": "ARE", + "name": "Arequipa" + }, + { + "code": "AYA", + "name": "Ayacucho" + }, + { + "code": "CAJ", + "name": "Cajamarca" + }, + { + "code": "CUS", + "name": "Cusco" + }, + { + "code": "HUV", + "name": "Huancavelica" + }, + { + "code": "HUC", + "name": "Huánuco" + }, + { + "code": "ICA", + "name": "Ica" + }, + { + "code": "JUN", + "name": "Junín" + }, + { + "code": "LAL", + "name": "La Libertad" + }, + { + "code": "LAM", + "name": "Lambayeque" + }, + { + "code": "LIM", + "name": "Lima" + }, + { + "code": "LOR", + "name": "Loreto" + }, + { + "code": "MDD", + "name": "Madre de Dios" + }, + { + "code": "MOQ", + "name": "Moquegua" + }, + { + "code": "PAS", + "name": "Pasco" + }, + { + "code": "PIU", + "name": "Piura" + }, + { + "code": "PUN", + "name": "Puno" + }, + { + "code": "SAM", + "name": "San Martín" + }, + { + "code": "TAC", + "name": "Tacna" + }, + { + "code": "TUM", + "name": "Tumbes" + }, + { + "code": "UCA", + "name": "Ucayali" + } + ] + }, + { + "code": "PY", + "name": "Paraguayan guaraní", + "currency_code": "PYG", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "PY-ASU", + "name": "Asunción" + }, + { + "code": "PY-1", + "name": "Concepción" + }, + { + "code": "PY-2", + "name": "San Pedro" + }, + { + "code": "PY-3", + "name": "Cordillera" + }, + { + "code": "PY-4", + "name": "Guairá" + }, + { + "code": "PY-5", + "name": "Caaguazú" + }, + { + "code": "PY-6", + "name": "Caazapá" + }, + { + "code": "PY-7", + "name": "Itapúa" + }, + { + "code": "PY-8", + "name": "Misiones" + }, + { + "code": "PY-9", + "name": "Paraguarí" + }, + { + "code": "PY-10", + "name": "Alto Paraná" + }, + { + "code": "PY-11", + "name": "Central" + }, + { + "code": "PY-12", + "name": "Ñeembucú" + }, + { + "code": "PY-13", + "name": "Amambay" + }, + { + "code": "PY-14", + "name": "Canindeyú" + }, + { + "code": "PY-15", + "name": "Presidente Hayes" + }, + { + "code": "PY-16", + "name": "Alto Paraguay" + }, + { + "code": "PY-17", + "name": "Boquerón" + } + ] + }, + { + "code": "SR", + "name": "Surinamese dollar", + "currency_code": "SRD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "UY", + "name": "Uruguayan peso", + "currency_code": "UYU", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "UY-AR", + "name": "Artigas" + }, + { + "code": "UY-CA", + "name": "Canelones" + }, + { + "code": "UY-CL", + "name": "Cerro Largo" + }, + { + "code": "UY-CO", + "name": "Colonia" + }, + { + "code": "UY-DU", + "name": "Durazno" + }, + { + "code": "UY-FS", + "name": "Flores" + }, + { + "code": "UY-FD", + "name": "Florida" + }, + { + "code": "UY-LA", + "name": "Lavalleja" + }, + { + "code": "UY-MA", + "name": "Maldonado" + }, + { + "code": "UY-MO", + "name": "Montevideo" + }, + { + "code": "UY-PA", + "name": "Paysandú" + }, + { + "code": "UY-RN", + "name": "Río Negro" + }, + { + "code": "UY-RV", + "name": "Rivera" + }, + { + "code": "UY-RO", + "name": "Rocha" + }, + { + "code": "UY-SA", + "name": "Salto" + }, + { + "code": "UY-SJ", + "name": "San José" + }, + { + "code": "UY-SO", + "name": "Soriano" + }, + { + "code": "UY-TA", + "name": "Tacuarembó" + }, + { + "code": "UY-TT", + "name": "Treinta y Tres" + } + ] + }, + { + "code": "VE", + "name": "Bolívar soberano", + "currency_code": "VES", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "VE-A", + "name": "Capital" + }, + { + "code": "VE-B", + "name": "Anzoátegui" + }, + { + "code": "VE-C", + "name": "Apure" + }, + { + "code": "VE-D", + "name": "Aragua" + }, + { + "code": "VE-E", + "name": "Barinas" + }, + { + "code": "VE-F", + "name": "Bolívar" + }, + { + "code": "VE-G", + "name": "Carabobo" + }, + { + "code": "VE-H", + "name": "Cojedes" + }, + { + "code": "VE-I", + "name": "Falcón" + }, + { + "code": "VE-J", + "name": "Guárico" + }, + { + "code": "VE-K", + "name": "Lara" + }, + { + "code": "VE-L", + "name": "Mérida" + }, + { + "code": "VE-M", + "name": "Miranda" + }, + { + "code": "VE-N", + "name": "Monagas" + }, + { + "code": "VE-O", + "name": "Nueva Esparta" + }, + { + "code": "VE-P", + "name": "Portuguesa" + }, + { + "code": "VE-R", + "name": "Sucre" + }, + { + "code": "VE-S", + "name": "Táchira" + }, + { + "code": "VE-T", + "name": "Trujillo" + }, + { + "code": "VE-U", + "name": "Yaracuy" + }, + { + "code": "VE-V", + "name": "Zulia" + }, + { + "code": "VE-W", + "name": "Federal Dependencies" + }, + { + "code": "VE-X", + "name": "La Guaira (Vargas)" + }, + { + "code": "VE-Y", + "name": "Delta Amacuro" + }, + { + "code": "VE-Z", + "name": "Amazonas" + } + ] + } + ], + }) + ]) + ); + }); + + test('can view continent data', async ({ + request + }) => { + // call API to retrieve a specific continent data + const response = await request.get('/wp-json/wc/v3/data/continents/eu'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "EU", + "name": "Europe", + "countries": [{ + "code": "AD", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AL", + "name": "Albanian lek", + "currency_code": "ALL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "AL-01", + "name": "Berat" + }, + { + "code": "AL-09", + "name": "Dibër" + }, + { + "code": "AL-02", + "name": "Durrës" + }, + { + "code": "AL-03", + "name": "Elbasan" + }, + { + "code": "AL-04", + "name": "Fier" + }, + { + "code": "AL-05", + "name": "Gjirokastër" + }, + { + "code": "AL-06", + "name": "Korçë" + }, + { + "code": "AL-07", + "name": "Kukës" + }, + { + "code": "AL-08", + "name": "Lezhë" + }, + { + "code": "AL-10", + "name": "Shkodër" + }, + { + "code": "AL-11", + "name": "Tirana" + }, + { + "code": "AL-12", + "name": "Vlorë" + } + ] + }, + { + "code": "AT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AX", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BA", + "name": "Bosnia and Herzegovina convertible mark", + "currency_code": "BAM", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BG", + "name": "Bulgarian lev", + "currency_code": "BGN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BG-01", + "name": "Blagoevgrad" + }, + { + "code": "BG-02", + "name": "Burgas" + }, + { + "code": "BG-08", + "name": "Dobrich" + }, + { + "code": "BG-07", + "name": "Gabrovo" + }, + { + "code": "BG-26", + "name": "Haskovo" + }, + { + "code": "BG-09", + "name": "Kardzhali" + }, + { + "code": "BG-10", + "name": "Kyustendil" + }, + { + "code": "BG-11", + "name": "Lovech" + }, + { + "code": "BG-12", + "name": "Montana" + }, + { + "code": "BG-13", + "name": "Pazardzhik" + }, + { + "code": "BG-14", + "name": "Pernik" + }, + { + "code": "BG-15", + "name": "Pleven" + }, + { + "code": "BG-16", + "name": "Plovdiv" + }, + { + "code": "BG-17", + "name": "Razgrad" + }, + { + "code": "BG-18", + "name": "Ruse" + }, + { + "code": "BG-27", + "name": "Shumen" + }, + { + "code": "BG-19", + "name": "Silistra" + }, + { + "code": "BG-20", + "name": "Sliven" + }, + { + "code": "BG-21", + "name": "Smolyan" + }, + { + "code": "BG-23", + "name": "Sofia District" + }, + { + "code": "BG-22", + "name": "Sofia" + }, + { + "code": "BG-24", + "name": "Stara Zagora" + }, + { + "code": "BG-25", + "name": "Targovishte" + }, + { + "code": "BG-03", + "name": "Varna" + }, + { + "code": "BG-04", + "name": "Veliko Tarnovo" + }, + { + "code": "BG-05", + "name": "Vidin" + }, + { + "code": "BG-06", + "name": "Vratsa" + }, + { + "code": "BG-28", + "name": "Yambol" + } + ] + }, + { + "code": "BY", + "name": "Belarusian ruble", + "currency_code": "BYN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CH", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Aargau" + }, + { + "code": "AR", + "name": "Appenzell Ausserrhoden" + }, + { + "code": "AI", + "name": "Appenzell Innerrhoden" + }, + { + "code": "BL", + "name": "Basel-Landschaft" + }, + { + "code": "BS", + "name": "Basel-Stadt" + }, + { + "code": "BE", + "name": "Bern" + }, + { + "code": "FR", + "name": "Fribourg" + }, + { + "code": "GE", + "name": "Geneva" + }, + { + "code": "GL", + "name": "Glarus" + }, + { + "code": "GR", + "name": "Graubünden" + }, + { + "code": "JU", + "name": "Jura" + }, + { + "code": "LU", + "name": "Luzern" + }, + { + "code": "NE", + "name": "Neuchâtel" + }, + { + "code": "NW", + "name": "Nidwalden" + }, + { + "code": "OW", + "name": "Obwalden" + }, + { + "code": "SH", + "name": "Schaffhausen" + }, + { + "code": "SZ", + "name": "Schwyz" + }, + { + "code": "SO", + "name": "Solothurn" + }, + { + "code": "SG", + "name": "St. Gallen" + }, + { + "code": "TG", + "name": "Thurgau" + }, + { + "code": "TI", + "name": "Ticino" + }, + { + "code": "UR", + "name": "Uri" + }, + { + "code": "VS", + "name": "Valais" + }, + { + "code": "VD", + "name": "Vaud" + }, + { + "code": "ZG", + "name": "Zug" + }, + { + "code": "ZH", + "name": "Zürich" + } + ] + }, + { + "code": "CZ", + "name": "Czech koruna", + "currency_code": "CZK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "DE-BW", + "name": "Baden-Württemberg" + }, + { + "code": "DE-BY", + "name": "Bavaria" + }, + { + "code": "DE-BE", + "name": "Berlin" + }, + { + "code": "DE-BB", + "name": "Brandenburg" + }, + { + "code": "DE-HB", + "name": "Bremen" + }, + { + "code": "DE-HH", + "name": "Hamburg" + }, + { + "code": "DE-HE", + "name": "Hesse" + }, + { + "code": "DE-MV", + "name": "Mecklenburg-Vorpommern" + }, + { + "code": "DE-NI", + "name": "Lower Saxony" + }, + { + "code": "DE-NW", + "name": "North Rhine-Westphalia" + }, + { + "code": "DE-RP", + "name": "Rhineland-Palatinate" + }, + { + "code": "DE-SL", + "name": "Saarland" + }, + { + "code": "DE-SN", + "name": "Saxony" + }, + { + "code": "DE-ST", + "name": "Saxony-Anhalt" + }, + { + "code": "DE-SH", + "name": "Schleswig-Holstein" + }, + { + "code": "DE-TH", + "name": "Thuringia" + } + ] + }, + { + "code": "DK", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "EE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ES", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "A Coruña" + }, + { + "code": "VI", + "name": "Araba/Álava" + }, + { + "code": "AB", + "name": "Albacete" + }, + { + "code": "A", + "name": "Alicante" + }, + { + "code": "AL", + "name": "Almería" + }, + { + "code": "O", + "name": "Asturias" + }, + { + "code": "AV", + "name": "Ávila" + }, + { + "code": "BA", + "name": "Badajoz" + }, + { + "code": "PM", + "name": "Baleares" + }, + { + "code": "B", + "name": "Barcelona" + }, + { + "code": "BU", + "name": "Burgos" + }, + { + "code": "CC", + "name": "Cáceres" + }, + { + "code": "CA", + "name": "Cádiz" + }, + { + "code": "S", + "name": "Cantabria" + }, + { + "code": "CS", + "name": "Castellón" + }, + { + "code": "CE", + "name": "Ceuta" + }, + { + "code": "CR", + "name": "Ciudad Real" + }, + { + "code": "CO", + "name": "Córdoba" + }, + { + "code": "CU", + "name": "Cuenca" + }, + { + "code": "GI", + "name": "Girona" + }, + { + "code": "GR", + "name": "Granada" + }, + { + "code": "GU", + "name": "Guadalajara" + }, + { + "code": "SS", + "name": "Gipuzkoa" + }, + { + "code": "H", + "name": "Huelva" + }, + { + "code": "HU", + "name": "Huesca" + }, + { + "code": "J", + "name": "Jaén" + }, + { + "code": "LO", + "name": "La Rioja" + }, + { + "code": "GC", + "name": "Las Palmas" + }, + { + "code": "LE", + "name": "León" + }, + { + "code": "L", + "name": "Lleida" + }, + { + "code": "LU", + "name": "Lugo" + }, + { + "code": "M", + "name": "Madrid" + }, + { + "code": "MA", + "name": "Málaga" + }, + { + "code": "ML", + "name": "Melilla" + }, + { + "code": "MU", + "name": "Murcia" + }, + { + "code": "NA", + "name": "Navarra" + }, + { + "code": "OR", + "name": "Ourense" + }, + { + "code": "P", + "name": "Palencia" + }, + { + "code": "PO", + "name": "Pontevedra" + }, + { + "code": "SA", + "name": "Salamanca" + }, + { + "code": "TF", + "name": "Santa Cruz de Tenerife" + }, + { + "code": "SG", + "name": "Segovia" + }, + { + "code": "SE", + "name": "Sevilla" + }, + { + "code": "SO", + "name": "Soria" + }, + { + "code": "T", + "name": "Tarragona" + }, + { + "code": "TE", + "name": "Teruel" + }, + { + "code": "TO", + "name": "Toledo" + }, + { + "code": "V", + "name": "Valencia" + }, + { + "code": "VA", + "name": "Valladolid" + }, + { + "code": "BI", + "name": "Biscay" + }, + { + "code": "ZA", + "name": "Zamora" + }, + { + "code": "Z", + "name": "Zaragoza" + } + ] + }, + { + "code": "FI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FO", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GB", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "foot", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "oz", + "states": [] + }, + { + "code": "GG", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GI", + "name": "Gibraltar pound", + "currency_code": "GIP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "I", + "name": "Attica" + }, + { + "code": "A", + "name": "East Macedonia and Thrace" + }, + { + "code": "B", + "name": "Central Macedonia" + }, + { + "code": "C", + "name": "West Macedonia" + }, + { + "code": "D", + "name": "Epirus" + }, + { + "code": "E", + "name": "Thessaly" + }, + { + "code": "F", + "name": "Ionian Islands" + }, + { + "code": "G", + "name": "West Greece" + }, + { + "code": "H", + "name": "Central Greece" + }, + { + "code": "J", + "name": "Peloponnese" + }, + { + "code": "K", + "name": "North Aegean" + }, + { + "code": "L", + "name": "South Aegean" + }, + { + "code": "M", + "name": "Crete" + } + ] + }, + { + "code": "HR", + "name": "Croatian kuna", + "currency_code": "HRK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "HU", + "name": "Hungarian forint", + "currency_code": "HUF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BK", + "name": "Bács-Kiskun" + }, + { + "code": "BE", + "name": "Békés" + }, + { + "code": "BA", + "name": "Baranya" + }, + { + "code": "BZ", + "name": "Borsod-Abaúj-Zemplén" + }, + { + "code": "BU", + "name": "Budapest" + }, + { + "code": "CS", + "name": "Csongrád-Csanád" + }, + { + "code": "FE", + "name": "Fejér" + }, + { + "code": "GS", + "name": "Győr-Moson-Sopron" + }, + { + "code": "HB", + "name": "Hajdú-Bihar" + }, + { + "code": "HE", + "name": "Heves" + }, + { + "code": "JN", + "name": "Jász-Nagykun-Szolnok" + }, + { + "code": "KE", + "name": "Komárom-Esztergom" + }, + { + "code": "NO", + "name": "Nógrád" + }, + { + "code": "PE", + "name": "Pest" + }, + { + "code": "SO", + "name": "Somogy" + }, + { + "code": "SZ", + "name": "Szabolcs-Szatmár-Bereg" + }, + { + "code": "TO", + "name": "Tolna" + }, + { + "code": "VA", + "name": "Vas" + }, + { + "code": "VE", + "name": "Veszprém" + }, + { + "code": "ZA", + "name": "Zala" + } + ] + }, + { + "code": "IE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CW", + "name": "Carlow" + }, + { + "code": "CN", + "name": "Cavan" + }, + { + "code": "CE", + "name": "Clare" + }, + { + "code": "CO", + "name": "Cork" + }, + { + "code": "DL", + "name": "Donegal" + }, + { + "code": "D", + "name": "Dublin" + }, + { + "code": "G", + "name": "Galway" + }, + { + "code": "KY", + "name": "Kerry" + }, + { + "code": "KE", + "name": "Kildare" + }, + { + "code": "KK", + "name": "Kilkenny" + }, + { + "code": "LS", + "name": "Laois" + }, + { + "code": "LM", + "name": "Leitrim" + }, + { + "code": "LK", + "name": "Limerick" + }, + { + "code": "LD", + "name": "Longford" + }, + { + "code": "LH", + "name": "Louth" + }, + { + "code": "MO", + "name": "Mayo" + }, + { + "code": "MH", + "name": "Meath" + }, + { + "code": "MN", + "name": "Monaghan" + }, + { + "code": "OY", + "name": "Offaly" + }, + { + "code": "RN", + "name": "Roscommon" + }, + { + "code": "SO", + "name": "Sligo" + }, + { + "code": "TA", + "name": "Tipperary" + }, + { + "code": "WD", + "name": "Waterford" + }, + { + "code": "WH", + "name": "Westmeath" + }, + { + "code": "WX", + "name": "Wexford" + }, + { + "code": "WW", + "name": "Wicklow" + } + ] + }, + { + "code": "IM", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IS", + "name": "Icelandic króna", + "currency_code": "ISK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Agrigento" + }, + { + "code": "AL", + "name": "Alessandria" + }, + { + "code": "AN", + "name": "Ancona" + }, + { + "code": "AO", + "name": "Aosta" + }, + { + "code": "AR", + "name": "Arezzo" + }, + { + "code": "AP", + "name": "Ascoli Piceno" + }, + { + "code": "AT", + "name": "Asti" + }, + { + "code": "AV", + "name": "Avellino" + }, + { + "code": "BA", + "name": "Bari" + }, + { + "code": "BT", + "name": "Barletta-Andria-Trani" + }, + { + "code": "BL", + "name": "Belluno" + }, + { + "code": "BN", + "name": "Benevento" + }, + { + "code": "BG", + "name": "Bergamo" + }, + { + "code": "BI", + "name": "Biella" + }, + { + "code": "BO", + "name": "Bologna" + }, + { + "code": "BZ", + "name": "Bolzano" + }, + { + "code": "BS", + "name": "Brescia" + }, + { + "code": "BR", + "name": "Brindisi" + }, + { + "code": "CA", + "name": "Cagliari" + }, + { + "code": "CL", + "name": "Caltanissetta" + }, + { + "code": "CB", + "name": "Campobasso" + }, + { + "code": "CE", + "name": "Caserta" + }, + { + "code": "CT", + "name": "Catania" + }, + { + "code": "CZ", + "name": "Catanzaro" + }, + { + "code": "CH", + "name": "Chieti" + }, + { + "code": "CO", + "name": "Como" + }, + { + "code": "CS", + "name": "Cosenza" + }, + { + "code": "CR", + "name": "Cremona" + }, + { + "code": "KR", + "name": "Crotone" + }, + { + "code": "CN", + "name": "Cuneo" + }, + { + "code": "EN", + "name": "Enna" + }, + { + "code": "FM", + "name": "Fermo" + }, + { + "code": "FE", + "name": "Ferrara" + }, + { + "code": "FI", + "name": "Firenze" + }, + { + "code": "FG", + "name": "Foggia" + }, + { + "code": "FC", + "name": "Forlì-Cesena" + }, + { + "code": "FR", + "name": "Frosinone" + }, + { + "code": "GE", + "name": "Genova" + }, + { + "code": "GO", + "name": "Gorizia" + }, + { + "code": "GR", + "name": "Grosseto" + }, + { + "code": "IM", + "name": "Imperia" + }, + { + "code": "IS", + "name": "Isernia" + }, + { + "code": "SP", + "name": "La Spezia" + }, + { + "code": "AQ", + "name": "L'Aquila" + }, + { + "code": "LT", + "name": "Latina" + }, + { + "code": "LE", + "name": "Lecce" + }, + { + "code": "LC", + "name": "Lecco" + }, + { + "code": "LI", + "name": "Livorno" + }, + { + "code": "LO", + "name": "Lodi" + }, + { + "code": "LU", + "name": "Lucca" + }, + { + "code": "MC", + "name": "Macerata" + }, + { + "code": "MN", + "name": "Mantova" + }, + { + "code": "MS", + "name": "Massa-Carrara" + }, + { + "code": "MT", + "name": "Matera" + }, + { + "code": "ME", + "name": "Messina" + }, + { + "code": "MI", + "name": "Milano" + }, + { + "code": "MO", + "name": "Modena" + }, + { + "code": "MB", + "name": "Monza e della Brianza" + }, + { + "code": "NA", + "name": "Napoli" + }, + { + "code": "NO", + "name": "Novara" + }, + { + "code": "NU", + "name": "Nuoro" + }, + { + "code": "OR", + "name": "Oristano" + }, + { + "code": "PD", + "name": "Padova" + }, + { + "code": "PA", + "name": "Palermo" + }, + { + "code": "PR", + "name": "Parma" + }, + { + "code": "PV", + "name": "Pavia" + }, + { + "code": "PG", + "name": "Perugia" + }, + { + "code": "PU", + "name": "Pesaro e Urbino" + }, + { + "code": "PE", + "name": "Pescara" + }, + { + "code": "PC", + "name": "Piacenza" + }, + { + "code": "PI", + "name": "Pisa" + }, + { + "code": "PT", + "name": "Pistoia" + }, + { + "code": "PN", + "name": "Pordenone" + }, + { + "code": "PZ", + "name": "Potenza" + }, + { + "code": "PO", + "name": "Prato" + }, + { + "code": "RG", + "name": "Ragusa" + }, + { + "code": "RA", + "name": "Ravenna" + }, + { + "code": "RC", + "name": "Reggio Calabria" + }, + { + "code": "RE", + "name": "Reggio Emilia" + }, + { + "code": "RI", + "name": "Rieti" + }, + { + "code": "RN", + "name": "Rimini" + }, + { + "code": "RM", + "name": "Roma" + }, + { + "code": "RO", + "name": "Rovigo" + }, + { + "code": "SA", + "name": "Salerno" + }, + { + "code": "SS", + "name": "Sassari" + }, + { + "code": "SV", + "name": "Savona" + }, + { + "code": "SI", + "name": "Siena" + }, + { + "code": "SR", + "name": "Siracusa" + }, + { + "code": "SO", + "name": "Sondrio" + }, + { + "code": "SU", + "name": "Sud Sardegna" + }, + { + "code": "TA", + "name": "Taranto" + }, + { + "code": "TE", + "name": "Teramo" + }, + { + "code": "TR", + "name": "Terni" + }, + { + "code": "TO", + "name": "Torino" + }, + { + "code": "TP", + "name": "Trapani" + }, + { + "code": "TN", + "name": "Trento" + }, + { + "code": "TV", + "name": "Treviso" + }, + { + "code": "TS", + "name": "Trieste" + }, + { + "code": "UD", + "name": "Udine" + }, + { + "code": "VA", + "name": "Varese" + }, + { + "code": "VE", + "name": "Venezia" + }, + { + "code": "VB", + "name": "Verbano-Cusio-Ossola" + }, + { + "code": "VC", + "name": "Vercelli" + }, + { + "code": "VR", + "name": "Verona" + }, + { + "code": "VV", + "name": "Vibo Valentia" + }, + { + "code": "VI", + "name": "Vicenza" + }, + { + "code": "VT", + "name": "Viterbo" + } + ] + }, + { + "code": "JE", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LI", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LU", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LV", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MC", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MD", + "name": "Moldovan leu", + "currency_code": "MDL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "Chișinău" + }, + { + "code": "BL", + "name": "Bălți" + }, + { + "code": "AN", + "name": "Anenii Noi" + }, + { + "code": "BS", + "name": "Basarabeasca" + }, + { + "code": "BR", + "name": "Briceni" + }, + { + "code": "CH", + "name": "Cahul" + }, + { + "code": "CT", + "name": "Cantemir" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Căușeni" + }, + { + "code": "CM", + "name": "Cimișlia" + }, + { + "code": "CR", + "name": "Criuleni" + }, + { + "code": "DN", + "name": "Dondușeni" + }, + { + "code": "DR", + "name": "Drochia" + }, + { + "code": "DB", + "name": "Dubăsari" + }, + { + "code": "ED", + "name": "Edineț" + }, + { + "code": "FL", + "name": "Fălești" + }, + { + "code": "FR", + "name": "Florești" + }, + { + "code": "GE", + "name": "UTA Găgăuzia" + }, + { + "code": "GL", + "name": "Glodeni" + }, + { + "code": "HN", + "name": "Hîncești" + }, + { + "code": "IL", + "name": "Ialoveni" + }, + { + "code": "LV", + "name": "Leova" + }, + { + "code": "NS", + "name": "Nisporeni" + }, + { + "code": "OC", + "name": "Ocnița" + }, + { + "code": "OR", + "name": "Orhei" + }, + { + "code": "RZ", + "name": "Rezina" + }, + { + "code": "RS", + "name": "Rîșcani" + }, + { + "code": "SG", + "name": "Sîngerei" + }, + { + "code": "SR", + "name": "Soroca" + }, + { + "code": "ST", + "name": "Strășeni" + }, + { + "code": "SD", + "name": "Șoldănești" + }, + { + "code": "SV", + "name": "Ștefan Vodă" + }, + { + "code": "TR", + "name": "Taraclia" + }, + { + "code": "TL", + "name": "Telenești" + }, + { + "code": "UN", + "name": "Ungheni" + } + ] + }, + { + "code": "ME", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MK", + "name": "Macedonian denar", + "currency_code": "MKD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NL", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NO", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PL", + "name": "Polish złoty", + "currency_code": "PLN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "RO", + "name": "Romanian leu", + "currency_code": "RON", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Alba" + }, + { + "code": "AR", + "name": "Arad" + }, + { + "code": "AG", + "name": "Argeș" + }, + { + "code": "BC", + "name": "Bacău" + }, + { + "code": "BH", + "name": "Bihor" + }, + { + "code": "BN", + "name": "Bistrița-Năsăud" + }, + { + "code": "BT", + "name": "Botoșani" + }, + { + "code": "BR", + "name": "Brăila" + }, + { + "code": "BV", + "name": "Brașov" + }, + { + "code": "B", + "name": "București" + }, + { + "code": "BZ", + "name": "Buzău" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Caraș-Severin" + }, + { + "code": "CJ", + "name": "Cluj" + }, + { + "code": "CT", + "name": "Constanța" + }, + { + "code": "CV", + "name": "Covasna" + }, + { + "code": "DB", + "name": "Dâmbovița" + }, + { + "code": "DJ", + "name": "Dolj" + }, + { + "code": "GL", + "name": "Galați" + }, + { + "code": "GR", + "name": "Giurgiu" + }, + { + "code": "GJ", + "name": "Gorj" + }, + { + "code": "HR", + "name": "Harghita" + }, + { + "code": "HD", + "name": "Hunedoara" + }, + { + "code": "IL", + "name": "Ialomița" + }, + { + "code": "IS", + "name": "Iași" + }, + { + "code": "IF", + "name": "Ilfov" + }, + { + "code": "MM", + "name": "Maramureș" + }, + { + "code": "MH", + "name": "Mehedinți" + }, + { + "code": "MS", + "name": "Mureș" + }, + { + "code": "NT", + "name": "Neamț" + }, + { + "code": "OT", + "name": "Olt" + }, + { + "code": "PH", + "name": "Prahova" + }, + { + "code": "SJ", + "name": "Sălaj" + }, + { + "code": "SM", + "name": "Satu Mare" + }, + { + "code": "SB", + "name": "Sibiu" + }, + { + "code": "SV", + "name": "Suceava" + }, + { + "code": "TR", + "name": "Teleorman" + }, + { + "code": "TM", + "name": "Timiș" + }, + { + "code": "TL", + "name": "Tulcea" + }, + { + "code": "VL", + "name": "Vâlcea" + }, + { + "code": "VS", + "name": "Vaslui" + }, + { + "code": "VN", + "name": "Vrancea" + } + ] + }, + { + "code": "RS", + "name": "Serbian dinar", + "currency_code": "RSD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "RS00", + "name": "Belgrade" + }, + { + "code": "RS14", + "name": "Bor" + }, + { + "code": "RS11", + "name": "Braničevo" + }, + { + "code": "RS02", + "name": "Central Banat" + }, + { + "code": "RS10", + "name": "Danube" + }, + { + "code": "RS23", + "name": "Jablanica" + }, + { + "code": "RS09", + "name": "Kolubara" + }, + { + "code": "RS08", + "name": "Mačva" + }, + { + "code": "RS17", + "name": "Morava" + }, + { + "code": "RS20", + "name": "Nišava" + }, + { + "code": "RS01", + "name": "North Bačka" + }, + { + "code": "RS03", + "name": "North Banat" + }, + { + "code": "RS24", + "name": "Pčinja" + }, + { + "code": "RS22", + "name": "Pirot" + }, + { + "code": "RS13", + "name": "Pomoravlje" + }, + { + "code": "RS19", + "name": "Rasina" + }, + { + "code": "RS18", + "name": "Raška" + }, + { + "code": "RS06", + "name": "South Bačka" + }, + { + "code": "RS04", + "name": "South Banat" + }, + { + "code": "RS07", + "name": "Srem" + }, + { + "code": "RS12", + "name": "Šumadija" + }, + { + "code": "RS21", + "name": "Toplica" + }, + { + "code": "RS05", + "name": "West Bačka" + }, + { + "code": "RS15", + "name": "Zaječar" + }, + { + "code": "RS16", + "name": "Zlatibor" + }, + { + "code": "RS25", + "name": "Kosovo" + }, + { + "code": "RS26", + "name": "Peć" + }, + { + "code": "RS27", + "name": "Prizren" + }, + { + "code": "RS28", + "name": "Kosovska Mitrovica" + }, + { + "code": "RS29", + "name": "Kosovo-Pomoravlje" + }, + { + "code": "RSKM", + "name": "Kosovo-Metohija" + }, + { + "code": "RSVO", + "name": "Vojvodina" + } + ] + }, + { + "code": "RU", + "name": "Russian ruble", + "currency_code": "RUB", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SE", + "name": "Swedish krona", + "currency_code": "SEK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SJ", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SK", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SM", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TR", + "name": "Turkish lira", + "currency_code": "TRY", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "TR01", + "name": "Adana" + }, + { + "code": "TR02", + "name": "Adıyaman" + }, + { + "code": "TR03", + "name": "Afyon" + }, + { + "code": "TR04", + "name": "Ağrı" + }, + { + "code": "TR05", + "name": "Amasya" + }, + { + "code": "TR06", + "name": "Ankara" + }, + { + "code": "TR07", + "name": "Antalya" + }, + { + "code": "TR08", + "name": "Artvin" + }, + { + "code": "TR09", + "name": "Aydın" + }, + { + "code": "TR10", + "name": "Balıkesir" + }, + { + "code": "TR11", + "name": "Bilecik" + }, + { + "code": "TR12", + "name": "Bingöl" + }, + { + "code": "TR13", + "name": "Bitlis" + }, + { + "code": "TR14", + "name": "Bolu" + }, + { + "code": "TR15", + "name": "Burdur" + }, + { + "code": "TR16", + "name": "Bursa" + }, + { + "code": "TR17", + "name": "Çanakkale" + }, + { + "code": "TR18", + "name": "Çankırı" + }, + { + "code": "TR19", + "name": "Çorum" + }, + { + "code": "TR20", + "name": "Denizli" + }, + { + "code": "TR21", + "name": "Diyarbakır" + }, + { + "code": "TR22", + "name": "Edirne" + }, + { + "code": "TR23", + "name": "Elazığ" + }, + { + "code": "TR24", + "name": "Erzincan" + }, + { + "code": "TR25", + "name": "Erzurum" + }, + { + "code": "TR26", + "name": "Eskişehir" + }, + { + "code": "TR27", + "name": "Gaziantep" + }, + { + "code": "TR28", + "name": "Giresun" + }, + { + "code": "TR29", + "name": "Gümüşhane" + }, + { + "code": "TR30", + "name": "Hakkari" + }, + { + "code": "TR31", + "name": "Hatay" + }, + { + "code": "TR32", + "name": "Isparta" + }, + { + "code": "TR33", + "name": "İçel" + }, + { + "code": "TR34", + "name": "İstanbul" + }, + { + "code": "TR35", + "name": "İzmir" + }, + { + "code": "TR36", + "name": "Kars" + }, + { + "code": "TR37", + "name": "Kastamonu" + }, + { + "code": "TR38", + "name": "Kayseri" + }, + { + "code": "TR39", + "name": "Kırklareli" + }, + { + "code": "TR40", + "name": "Kırşehir" + }, + { + "code": "TR41", + "name": "Kocaeli" + }, + { + "code": "TR42", + "name": "Konya" + }, + { + "code": "TR43", + "name": "Kütahya" + }, + { + "code": "TR44", + "name": "Malatya" + }, + { + "code": "TR45", + "name": "Manisa" + }, + { + "code": "TR46", + "name": "Kahramanmaraş" + }, + { + "code": "TR47", + "name": "Mardin" + }, + { + "code": "TR48", + "name": "Muğla" + }, + { + "code": "TR49", + "name": "Muş" + }, + { + "code": "TR50", + "name": "Nevşehir" + }, + { + "code": "TR51", + "name": "Niğde" + }, + { + "code": "TR52", + "name": "Ordu" + }, + { + "code": "TR53", + "name": "Rize" + }, + { + "code": "TR54", + "name": "Sakarya" + }, + { + "code": "TR55", + "name": "Samsun" + }, + { + "code": "TR56", + "name": "Siirt" + }, + { + "code": "TR57", + "name": "Sinop" + }, + { + "code": "TR58", + "name": "Sivas" + }, + { + "code": "TR59", + "name": "Tekirdağ" + }, + { + "code": "TR60", + "name": "Tokat" + }, + { + "code": "TR61", + "name": "Trabzon" + }, + { + "code": "TR62", + "name": "Tunceli" + }, + { + "code": "TR63", + "name": "Şanlıurfa" + }, + { + "code": "TR64", + "name": "Uşak" + }, + { + "code": "TR65", + "name": "Van" + }, + { + "code": "TR66", + "name": "Yozgat" + }, + { + "code": "TR67", + "name": "Zonguldak" + }, + { + "code": "TR68", + "name": "Aksaray" + }, + { + "code": "TR69", + "name": "Bayburt" + }, + { + "code": "TR70", + "name": "Karaman" + }, + { + "code": "TR71", + "name": "Kırıkkale" + }, + { + "code": "TR72", + "name": "Batman" + }, + { + "code": "TR73", + "name": "Şırnak" + }, + { + "code": "TR74", + "name": "Bartın" + }, + { + "code": "TR75", + "name": "Ardahan" + }, + { + "code": "TR76", + "name": "Iğdır" + }, + { + "code": "TR77", + "name": "Yalova" + }, + { + "code": "TR78", + "name": "Karabük" + }, + { + "code": "TR79", + "name": "Kilis" + }, + { + "code": "TR80", + "name": "Osmaniye" + }, + { + "code": "TR81", + "name": "Düzce" + } + ] + }, + { + "code": "UA", + "name": "Ukrainian hryvnia", + "currency_code": "UAH", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "VN", + "name": "Vinnytsia Oblast" + }, + { + "code": "VL", + "name": "Volyn Oblast" + }, + { + "code": "DP", + "name": "Dnipropetrovsk Oblast" + }, + { + "code": "DT", + "name": "Donetsk Oblast" + }, + { + "code": "ZT", + "name": "Zhytomyr Oblast" + }, + { + "code": "ZK", + "name": "Zakarpattia Oblast" + }, + { + "code": "ZP", + "name": "Zaporizhzhia Oblast" + }, + { + "code": "IF", + "name": "Ivano-Frankivsk Oblast" + }, + { + "code": "KV", + "name": "Kyiv Oblast" + }, + { + "code": "KH", + "name": "Kirovohrad Oblast" + }, + { + "code": "LH", + "name": "Luhansk Oblast" + }, + { + "code": "LV", + "name": "Lviv Oblast" + }, + { + "code": "MY", + "name": "Mykolaiv Oblast" + }, + { + "code": "OD", + "name": "Odessa Oblast" + }, + { + "code": "PL", + "name": "Poltava Oblast" + }, + { + "code": "RV", + "name": "Rivne Oblast" + }, + { + "code": "SM", + "name": "Sumy Oblast" + }, + { + "code": "TP", + "name": "Ternopil Oblast" + }, + { + "code": "KK", + "name": "Kharkiv Oblast" + }, + { + "code": "KS", + "name": "Kherson Oblast" + }, + { + "code": "KM", + "name": "Khmelnytskyi Oblast" + }, + { + "code": "CK", + "name": "Cherkasy Oblast" + }, + { + "code": "CH", + "name": "Chernihiv Oblast" + }, + { + "code": "CV", + "name": "Chernivtsi Oblast" + } + ] + }, + { + "code": "VA", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + } + ], + }) + ); + }); + + test('can view all countries', async ({ + request + }) => { + // call API to retrieve all countries + const response = await request.get('/wp-json/wc/v3/data/countries'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AF", + "name": "Afghanistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AX", + "name": "Åland Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AL", + "name": "Albania", + "states": [{ + "code": "AL-01", + "name": "Berat" + }, + { + "code": "AL-09", + "name": "Dibër" + }, + { + "code": "AL-02", + "name": "Durrës" + }, + { + "code": "AL-03", + "name": "Elbasan" + }, + { + "code": "AL-04", + "name": "Fier" + }, + { + "code": "AL-05", + "name": "Gjirokastër" + }, + { + "code": "AL-06", + "name": "Korçë" + }, + { + "code": "AL-07", + "name": "Kukës" + }, + { + "code": "AL-08", + "name": "Lezhë" + }, + { + "code": "AL-10", + "name": "Shkodër" + }, + { + "code": "AL-11", + "name": "Tirana" + }, + { + "code": "AL-12", + "name": "Vlorë" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DZ", + "name": "Algeria", + "states": [{ + "code": "DZ-01", + "name": "Adrar" + }, + { + "code": "DZ-02", + "name": "Chlef" + }, + { + "code": "DZ-03", + "name": "Laghouat" + }, + { + "code": "DZ-04", + "name": "Oum El Bouaghi" + }, + { + "code": "DZ-05", + "name": "Batna" + }, + { + "code": "DZ-06", + "name": "Béjaïa" + }, + { + "code": "DZ-07", + "name": "Biskra" + }, + { + "code": "DZ-08", + "name": "Béchar" + }, + { + "code": "DZ-09", + "name": "Blida" + }, + { + "code": "DZ-10", + "name": "Bouira" + }, + { + "code": "DZ-11", + "name": "Tamanghasset" + }, + { + "code": "DZ-12", + "name": "Tébessa" + }, + { + "code": "DZ-13", + "name": "Tlemcen" + }, + { + "code": "DZ-14", + "name": "Tiaret" + }, + { + "code": "DZ-15", + "name": "Tizi Ouzou" + }, + { + "code": "DZ-16", + "name": "Algiers" + }, + { + "code": "DZ-17", + "name": "Djelfa" + }, + { + "code": "DZ-18", + "name": "Jijel" + }, + { + "code": "DZ-19", + "name": "Sétif" + }, + { + "code": "DZ-20", + "name": "Saïda" + }, + { + "code": "DZ-21", + "name": "Skikda" + }, + { + "code": "DZ-22", + "name": "Sidi Bel Abbès" + }, + { + "code": "DZ-23", + "name": "Annaba" + }, + { + "code": "DZ-24", + "name": "Guelma" + }, + { + "code": "DZ-25", + "name": "Constantine" + }, + { + "code": "DZ-26", + "name": "Médéa" + }, + { + "code": "DZ-27", + "name": "Mostaganem" + }, + { + "code": "DZ-28", + "name": "M’Sila" + }, + { + "code": "DZ-29", + "name": "Mascara" + }, + { + "code": "DZ-30", + "name": "Ouargla" + }, + { + "code": "DZ-31", + "name": "Oran" + }, + { + "code": "DZ-32", + "name": "El Bayadh" + }, + { + "code": "DZ-33", + "name": "Illizi" + }, + { + "code": "DZ-34", + "name": "Bordj Bou Arréridj" + }, + { + "code": "DZ-35", + "name": "Boumerdès" + }, + { + "code": "DZ-36", + "name": "El Tarf" + }, + { + "code": "DZ-37", + "name": "Tindouf" + }, + { + "code": "DZ-38", + "name": "Tissemsilt" + }, + { + "code": "DZ-39", + "name": "El Oued" + }, + { + "code": "DZ-40", + "name": "Khenchela" + }, + { + "code": "DZ-41", + "name": "Souk Ahras" + }, + { + "code": "DZ-42", + "name": "Tipasa" + }, + { + "code": "DZ-43", + "name": "Mila" + }, + { + "code": "DZ-44", + "name": "Aïn Defla" + }, + { + "code": "DZ-45", + "name": "Naama" + }, + { + "code": "DZ-46", + "name": "Aïn Témouchent" + }, + { + "code": "DZ-47", + "name": "Ghardaïa" + }, + { + "code": "DZ-48", + "name": "Relizane" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AS", + "name": "American Samoa", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AD", + "name": "Andorra", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AO", + "name": "Angola", + "states": [{ + "code": "BGO", + "name": "Bengo" + }, + { + "code": "BLU", + "name": "Benguela" + }, + { + "code": "BIE", + "name": "Bié" + }, + { + "code": "CAB", + "name": "Cabinda" + }, + { + "code": "CNN", + "name": "Cunene" + }, + { + "code": "HUA", + "name": "Huambo" + }, + { + "code": "HUI", + "name": "Huíla" + }, + { + "code": "CCU", + "name": "Kuando Kubango" + }, + { + "code": "CNO", + "name": "Kwanza-Norte" + }, + { + "code": "CUS", + "name": "Kwanza-Sul" + }, + { + "code": "LUA", + "name": "Luanda" + }, + { + "code": "LNO", + "name": "Lunda-Norte" + }, + { + "code": "LSU", + "name": "Lunda-Sul" + }, + { + "code": "MAL", + "name": "Malanje" + }, + { + "code": "MOX", + "name": "Moxico" + }, + { + "code": "NAM", + "name": "Namibe" + }, + { + "code": "UIG", + "name": "Uíge" + }, + { + "code": "ZAI", + "name": "Zaire" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AI", + "name": "Anguilla", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AQ", + "name": "Antarctica", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AG", + "name": "Antigua and Barbuda", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AR", + "name": "Argentina", + "states": [{ + "code": "C", + "name": "Ciudad Autónoma de Buenos Aires" + }, + { + "code": "B", + "name": "Buenos Aires" + }, + { + "code": "K", + "name": "Catamarca" + }, + { + "code": "H", + "name": "Chaco" + }, + { + "code": "U", + "name": "Chubut" + }, + { + "code": "X", + "name": "Córdoba" + }, + { + "code": "W", + "name": "Corrientes" + }, + { + "code": "E", + "name": "Entre Ríos" + }, + { + "code": "P", + "name": "Formosa" + }, + { + "code": "Y", + "name": "Jujuy" + }, + { + "code": "L", + "name": "La Pampa" + }, + { + "code": "F", + "name": "La Rioja" + }, + { + "code": "M", + "name": "Mendoza" + }, + { + "code": "N", + "name": "Misiones" + }, + { + "code": "Q", + "name": "Neuquén" + }, + { + "code": "R", + "name": "Río Negro" + }, + { + "code": "A", + "name": "Salta" + }, + { + "code": "J", + "name": "San Juan" + }, + { + "code": "D", + "name": "San Luis" + }, + { + "code": "Z", + "name": "Santa Cruz" + }, + { + "code": "S", + "name": "Santa Fe" + }, + { + "code": "G", + "name": "Santiago del Estero" + }, + { + "code": "V", + "name": "Tierra del Fuego" + }, + { + "code": "T", + "name": "Tucumán" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AM", + "name": "Armenia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AW", + "name": "Aruba", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AU", + "name": "Australia", + "states": [{ + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "code": "NSW", + "name": "New South Wales" + }, + { + "code": "NT", + "name": "Northern Territory" + }, + { + "code": "QLD", + "name": "Queensland" + }, + { + "code": "SA", + "name": "South Australia" + }, + { + "code": "TAS", + "name": "Tasmania" + }, + { + "code": "VIC", + "name": "Victoria" + }, + { + "code": "WA", + "name": "Western Australia" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AT", + "name": "Austria", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AZ", + "name": "Azerbaijan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BS", + "name": "Bahamas", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BH", + "name": "Bahrain", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BD", + "name": "Bangladesh", + "states": [{ + "code": "BD-05", + "name": "Bagerhat" + }, + { + "code": "BD-01", + "name": "Bandarban" + }, + { + "code": "BD-02", + "name": "Barguna" + }, + { + "code": "BD-06", + "name": "Barishal" + }, + { + "code": "BD-07", + "name": "Bhola" + }, + { + "code": "BD-03", + "name": "Bogura" + }, + { + "code": "BD-04", + "name": "Brahmanbaria" + }, + { + "code": "BD-09", + "name": "Chandpur" + }, + { + "code": "BD-10", + "name": "Chattogram" + }, + { + "code": "BD-12", + "name": "Chuadanga" + }, + { + "code": "BD-11", + "name": "Cox's Bazar" + }, + { + "code": "BD-08", + "name": "Cumilla" + }, + { + "code": "BD-13", + "name": "Dhaka" + }, + { + "code": "BD-14", + "name": "Dinajpur" + }, + { + "code": "BD-15", + "name": "Faridpur " + }, + { + "code": "BD-16", + "name": "Feni" + }, + { + "code": "BD-19", + "name": "Gaibandha" + }, + { + "code": "BD-18", + "name": "Gazipur" + }, + { + "code": "BD-17", + "name": "Gopalganj" + }, + { + "code": "BD-20", + "name": "Habiganj" + }, + { + "code": "BD-21", + "name": "Jamalpur" + }, + { + "code": "BD-22", + "name": "Jashore" + }, + { + "code": "BD-25", + "name": "Jhalokati" + }, + { + "code": "BD-23", + "name": "Jhenaidah" + }, + { + "code": "BD-24", + "name": "Joypurhat" + }, + { + "code": "BD-29", + "name": "Khagrachhari" + }, + { + "code": "BD-27", + "name": "Khulna" + }, + { + "code": "BD-26", + "name": "Kishoreganj" + }, + { + "code": "BD-28", + "name": "Kurigram" + }, + { + "code": "BD-30", + "name": "Kushtia" + }, + { + "code": "BD-31", + "name": "Lakshmipur" + }, + { + "code": "BD-32", + "name": "Lalmonirhat" + }, + { + "code": "BD-36", + "name": "Madaripur" + }, + { + "code": "BD-37", + "name": "Magura" + }, + { + "code": "BD-33", + "name": "Manikganj " + }, + { + "code": "BD-39", + "name": "Meherpur" + }, + { + "code": "BD-38", + "name": "Moulvibazar" + }, + { + "code": "BD-35", + "name": "Munshiganj" + }, + { + "code": "BD-34", + "name": "Mymensingh" + }, + { + "code": "BD-48", + "name": "Naogaon" + }, + { + "code": "BD-43", + "name": "Narail" + }, + { + "code": "BD-40", + "name": "Narayanganj" + }, + { + "code": "BD-42", + "name": "Narsingdi" + }, + { + "code": "BD-44", + "name": "Natore" + }, + { + "code": "BD-45", + "name": "Nawabganj" + }, + { + "code": "BD-41", + "name": "Netrakona" + }, + { + "code": "BD-46", + "name": "Nilphamari" + }, + { + "code": "BD-47", + "name": "Noakhali" + }, + { + "code": "BD-49", + "name": "Pabna" + }, + { + "code": "BD-52", + "name": "Panchagarh" + }, + { + "code": "BD-51", + "name": "Patuakhali" + }, + { + "code": "BD-50", + "name": "Pirojpur" + }, + { + "code": "BD-53", + "name": "Rajbari" + }, + { + "code": "BD-54", + "name": "Rajshahi" + }, + { + "code": "BD-56", + "name": "Rangamati" + }, + { + "code": "BD-55", + "name": "Rangpur" + }, + { + "code": "BD-58", + "name": "Satkhira" + }, + { + "code": "BD-62", + "name": "Shariatpur" + }, + { + "code": "BD-57", + "name": "Sherpur" + }, + { + "code": "BD-59", + "name": "Sirajganj" + }, + { + "code": "BD-61", + "name": "Sunamganj" + }, + { + "code": "BD-60", + "name": "Sylhet" + }, + { + "code": "BD-63", + "name": "Tangail" + }, + { + "code": "BD-64", + "name": "Thakurgaon" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BB", + "name": "Barbados", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BY", + "name": "Belarus", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PW", + "name": "Belau", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BE", + "name": "Belgium", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BZ", + "name": "Belize", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BJ", + "name": "Benin", + "states": [{ + "code": "AL", + "name": "Alibori" + }, + { + "code": "AK", + "name": "Atakora" + }, + { + "code": "AQ", + "name": "Atlantique" + }, + { + "code": "BO", + "name": "Borgou" + }, + { + "code": "CO", + "name": "Collines" + }, + { + "code": "KO", + "name": "Kouffo" + }, + { + "code": "DO", + "name": "Donga" + }, + { + "code": "LI", + "name": "Littoral" + }, + { + "code": "MO", + "name": "Mono" + }, + { + "code": "OU", + "name": "Ouémé" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "ZO", + "name": "Zou" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BM", + "name": "Bermuda", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BT", + "name": "Bhutan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BO", + "name": "Bolivia", + "states": [{ + "code": "BO-B", + "name": "Beni" + }, + { + "code": "BO-H", + "name": "Chuquisaca" + }, + { + "code": "BO-C", + "name": "Cochabamba" + }, + { + "code": "BO-L", + "name": "La Paz" + }, + { + "code": "BO-O", + "name": "Oruro" + }, + { + "code": "BO-N", + "name": "Pando" + }, + { + "code": "BO-P", + "name": "Potosí" + }, + { + "code": "BO-S", + "name": "Santa Cruz" + }, + { + "code": "BO-T", + "name": "Tarija" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BQ", + "name": "Bonaire, Saint Eustatius and Saba", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BA", + "name": "Bosnia and Herzegovina", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BW", + "name": "Botswana", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BV", + "name": "Bouvet Island", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BR", + "name": "Brazil", + "states": [{ + "code": "AC", + "name": "Acre" + }, + { + "code": "AL", + "name": "Alagoas" + }, + { + "code": "AP", + "name": "Amapá" + }, + { + "code": "AM", + "name": "Amazonas" + }, + { + "code": "BA", + "name": "Bahia" + }, + { + "code": "CE", + "name": "Ceará" + }, + { + "code": "DF", + "name": "Distrito Federal" + }, + { + "code": "ES", + "name": "Espírito Santo" + }, + { + "code": "GO", + "name": "Goiás" + }, + { + "code": "MA", + "name": "Maranhão" + }, + { + "code": "MT", + "name": "Mato Grosso" + }, + { + "code": "MS", + "name": "Mato Grosso do Sul" + }, + { + "code": "MG", + "name": "Minas Gerais" + }, + { + "code": "PA", + "name": "Pará" + }, + { + "code": "PB", + "name": "Paraíba" + }, + { + "code": "PR", + "name": "Paraná" + }, + { + "code": "PE", + "name": "Pernambuco" + }, + { + "code": "PI", + "name": "Piauí" + }, + { + "code": "RJ", + "name": "Rio de Janeiro" + }, + { + "code": "RN", + "name": "Rio Grande do Norte" + }, + { + "code": "RS", + "name": "Rio Grande do Sul" + }, + { + "code": "RO", + "name": "Rondônia" + }, + { + "code": "RR", + "name": "Roraima" + }, + { + "code": "SC", + "name": "Santa Catarina" + }, + { + "code": "SP", + "name": "São Paulo" + }, + { + "code": "SE", + "name": "Sergipe" + }, + { + "code": "TO", + "name": "Tocantins" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IO", + "name": "British Indian Ocean Territory", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BN", + "name": "Brunei", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BG", + "name": "Bulgaria", + "states": [{ + "code": "BG-01", + "name": "Blagoevgrad" + }, + { + "code": "BG-02", + "name": "Burgas" + }, + { + "code": "BG-08", + "name": "Dobrich" + }, + { + "code": "BG-07", + "name": "Gabrovo" + }, + { + "code": "BG-26", + "name": "Haskovo" + }, + { + "code": "BG-09", + "name": "Kardzhali" + }, + { + "code": "BG-10", + "name": "Kyustendil" + }, + { + "code": "BG-11", + "name": "Lovech" + }, + { + "code": "BG-12", + "name": "Montana" + }, + { + "code": "BG-13", + "name": "Pazardzhik" + }, + { + "code": "BG-14", + "name": "Pernik" + }, + { + "code": "BG-15", + "name": "Pleven" + }, + { + "code": "BG-16", + "name": "Plovdiv" + }, + { + "code": "BG-17", + "name": "Razgrad" + }, + { + "code": "BG-18", + "name": "Ruse" + }, + { + "code": "BG-27", + "name": "Shumen" + }, + { + "code": "BG-19", + "name": "Silistra" + }, + { + "code": "BG-20", + "name": "Sliven" + }, + { + "code": "BG-21", + "name": "Smolyan" + }, + { + "code": "BG-23", + "name": "Sofia District" + }, + { + "code": "BG-22", + "name": "Sofia" + }, + { + "code": "BG-24", + "name": "Stara Zagora" + }, + { + "code": "BG-25", + "name": "Targovishte" + }, + { + "code": "BG-03", + "name": "Varna" + }, + { + "code": "BG-04", + "name": "Veliko Tarnovo" + }, + { + "code": "BG-05", + "name": "Vidin" + }, + { + "code": "BG-06", + "name": "Vratsa" + }, + { + "code": "BG-28", + "name": "Yambol" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BF", + "name": "Burkina Faso", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BI", + "name": "Burundi", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KH", + "name": "Cambodia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CM", + "name": "Cameroon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CA", + "name": "Canada", + "states": [{ + "code": "AB", + "name": "Alberta" + }, + { + "code": "BC", + "name": "British Columbia" + }, + { + "code": "MB", + "name": "Manitoba" + }, + { + "code": "NB", + "name": "New Brunswick" + }, + { + "code": "NL", + "name": "Newfoundland and Labrador" + }, + { + "code": "NT", + "name": "Northwest Territories" + }, + { + "code": "NS", + "name": "Nova Scotia" + }, + { + "code": "NU", + "name": "Nunavut" + }, + { + "code": "ON", + "name": "Ontario" + }, + { + "code": "PE", + "name": "Prince Edward Island" + }, + { + "code": "QC", + "name": "Quebec" + }, + { + "code": "SK", + "name": "Saskatchewan" + }, + { + "code": "YT", + "name": "Yukon Territory" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CV", + "name": "Cape Verde", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KY", + "name": "Cayman Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CF", + "name": "Central African Republic", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TD", + "name": "Chad", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CL", + "name": "Chile", + "states": [{ + "code": "CL-AI", + "name": "Aisén del General Carlos Ibañez del Campo" + }, + { + "code": "CL-AN", + "name": "Antofagasta" + }, + { + "code": "CL-AP", + "name": "Arica y Parinacota" + }, + { + "code": "CL-AR", + "name": "La Araucanía" + }, + { + "code": "CL-AT", + "name": "Atacama" + }, + { + "code": "CL-BI", + "name": "Biobío" + }, + { + "code": "CL-CO", + "name": "Coquimbo" + }, + { + "code": "CL-LI", + "name": "Libertador General Bernardo O'Higgins" + }, + { + "code": "CL-LL", + "name": "Los Lagos" + }, + { + "code": "CL-LR", + "name": "Los Ríos" + }, + { + "code": "CL-MA", + "name": "Magallanes" + }, + { + "code": "CL-ML", + "name": "Maule" + }, + { + "code": "CL-NB", + "name": "Ñuble" + }, + { + "code": "CL-RM", + "name": "Región Metropolitana de Santiago" + }, + { + "code": "CL-TA", + "name": "Tarapacá" + }, + { + "code": "CL-VS", + "name": "Valparaíso" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CN", + "name": "China", + "states": [{ + "code": "CN1", + "name": "Yunnan / 云南" + }, + { + "code": "CN2", + "name": "Beijing / 北京" + }, + { + "code": "CN3", + "name": "Tianjin / 天津" + }, + { + "code": "CN4", + "name": "Hebei / 河北" + }, + { + "code": "CN5", + "name": "Shanxi / 山西" + }, + { + "code": "CN6", + "name": "Inner Mongolia / 內蒙古" + }, + { + "code": "CN7", + "name": "Liaoning / 辽宁" + }, + { + "code": "CN8", + "name": "Jilin / 吉林" + }, + { + "code": "CN9", + "name": "Heilongjiang / 黑龙江" + }, + { + "code": "CN10", + "name": "Shanghai / 上海" + }, + { + "code": "CN11", + "name": "Jiangsu / 江苏" + }, + { + "code": "CN12", + "name": "Zhejiang / 浙江" + }, + { + "code": "CN13", + "name": "Anhui / 安徽" + }, + { + "code": "CN14", + "name": "Fujian / 福建" + }, + { + "code": "CN15", + "name": "Jiangxi / 江西" + }, + { + "code": "CN16", + "name": "Shandong / 山东" + }, + { + "code": "CN17", + "name": "Henan / 河南" + }, + { + "code": "CN18", + "name": "Hubei / 湖北" + }, + { + "code": "CN19", + "name": "Hunan / 湖南" + }, + { + "code": "CN20", + "name": "Guangdong / 广东" + }, + { + "code": "CN21", + "name": "Guangxi Zhuang / 广西壮族" + }, + { + "code": "CN22", + "name": "Hainan / 海南" + }, + { + "code": "CN23", + "name": "Chongqing / 重庆" + }, + { + "code": "CN24", + "name": "Sichuan / 四川" + }, + { + "code": "CN25", + "name": "Guizhou / 贵州" + }, + { + "code": "CN26", + "name": "Shaanxi / 陕西" + }, + { + "code": "CN27", + "name": "Gansu / 甘肃" + }, + { + "code": "CN28", + "name": "Qinghai / 青海" + }, + { + "code": "CN29", + "name": "Ningxia Hui / 宁夏" + }, + { + "code": "CN30", + "name": "Macao / 澳门" + }, + { + "code": "CN31", + "name": "Tibet / 西藏" + }, + { + "code": "CN32", + "name": "Xinjiang / 新疆" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CX", + "name": "Christmas Island", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CC", + "name": "Cocos (Keeling) Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CO", + "name": "Colombia", + "states": [{ + "code": "CO-AMA", + "name": "Amazonas" + }, + { + "code": "CO-ANT", + "name": "Antioquia" + }, + { + "code": "CO-ARA", + "name": "Arauca" + }, + { + "code": "CO-ATL", + "name": "Atlántico" + }, + { + "code": "CO-BOL", + "name": "Bolívar" + }, + { + "code": "CO-BOY", + "name": "Boyacá" + }, + { + "code": "CO-CAL", + "name": "Caldas" + }, + { + "code": "CO-CAQ", + "name": "Caquetá" + }, + { + "code": "CO-CAS", + "name": "Casanare" + }, + { + "code": "CO-CAU", + "name": "Cauca" + }, + { + "code": "CO-CES", + "name": "Cesar" + }, + { + "code": "CO-CHO", + "name": "Chocó" + }, + { + "code": "CO-COR", + "name": "Córdoba" + }, + { + "code": "CO-CUN", + "name": "Cundinamarca" + }, + { + "code": "CO-DC", + "name": "Capital District" + }, + { + "code": "CO-GUA", + "name": "Guainía" + }, + { + "code": "CO-GUV", + "name": "Guaviare" + }, + { + "code": "CO-HUI", + "name": "Huila" + }, + { + "code": "CO-LAG", + "name": "La Guajira" + }, + { + "code": "CO-MAG", + "name": "Magdalena" + }, + { + "code": "CO-MET", + "name": "Meta" + }, + { + "code": "CO-NAR", + "name": "Nariño" + }, + { + "code": "CO-NSA", + "name": "Norte de Santander" + }, + { + "code": "CO-PUT", + "name": "Putumayo" + }, + { + "code": "CO-QUI", + "name": "Quindío" + }, + { + "code": "CO-RIS", + "name": "Risaralda" + }, + { + "code": "CO-SAN", + "name": "Santander" + }, + { + "code": "CO-SAP", + "name": "San Andrés & Providencia" + }, + { + "code": "CO-SUC", + "name": "Sucre" + }, + { + "code": "CO-TOL", + "name": "Tolima" + }, + { + "code": "CO-VAC", + "name": "Valle del Cauca" + }, + { + "code": "CO-VAU", + "name": "Vaupés" + }, + { + "code": "CO-VID", + "name": "Vichada" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KM", + "name": "Comoros", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CG", + "name": "Congo (Brazzaville)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CD", + "name": "Congo (Kinshasa)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CK", + "name": "Cook Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CR", + "name": "Costa Rica", + "states": [{ + "code": "CR-A", + "name": "Alajuela" + }, + { + "code": "CR-C", + "name": "Cartago" + }, + { + "code": "CR-G", + "name": "Guanacaste" + }, + { + "code": "CR-H", + "name": "Heredia" + }, + { + "code": "CR-L", + "name": "Limón" + }, + { + "code": "CR-P", + "name": "Puntarenas" + }, + { + "code": "CR-SJ", + "name": "San José" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HR", + "name": "Croatia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CU", + "name": "Cuba", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CW", + "name": "Curaçao", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CY", + "name": "Cyprus", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CZ", + "name": "Czech Republic", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DK", + "name": "Denmark", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DJ", + "name": "Djibouti", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DM", + "name": "Dominica", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DO", + "name": "Dominican Republic", + "states": [{ + "code": "DO-01", + "name": "Distrito Nacional" + }, + { + "code": "DO-02", + "name": "Azua" + }, + { + "code": "DO-03", + "name": "Baoruco" + }, + { + "code": "DO-04", + "name": "Barahona" + }, + { + "code": "DO-33", + "name": "Cibao Nordeste" + }, + { + "code": "DO-34", + "name": "Cibao Noroeste" + }, + { + "code": "DO-35", + "name": "Cibao Norte" + }, + { + "code": "DO-36", + "name": "Cibao Sur" + }, + { + "code": "DO-05", + "name": "Dajabón" + }, + { + "code": "DO-06", + "name": "Duarte" + }, + { + "code": "DO-08", + "name": "El Seibo" + }, + { + "code": "DO-37", + "name": "El Valle" + }, + { + "code": "DO-07", + "name": "Elías Piña" + }, + { + "code": "DO-38", + "name": "Enriquillo" + }, + { + "code": "DO-09", + "name": "Espaillat" + }, + { + "code": "DO-30", + "name": "Hato Mayor" + }, + { + "code": "DO-19", + "name": "Hermanas Mirabal" + }, + { + "code": "DO-39", + "name": "Higüamo" + }, + { + "code": "DO-10", + "name": "Independencia" + }, + { + "code": "DO-11", + "name": "La Altagracia" + }, + { + "code": "DO-12", + "name": "La Romana" + }, + { + "code": "DO-13", + "name": "La Vega" + }, + { + "code": "DO-14", + "name": "María Trinidad Sánchez" + }, + { + "code": "DO-28", + "name": "Monseñor Nouel" + }, + { + "code": "DO-15", + "name": "Monte Cristi" + }, + { + "code": "DO-29", + "name": "Monte Plata" + }, + { + "code": "DO-40", + "name": "Ozama" + }, + { + "code": "DO-16", + "name": "Pedernales" + }, + { + "code": "DO-17", + "name": "Peravia" + }, + { + "code": "DO-18", + "name": "Puerto Plata" + }, + { + "code": "DO-20", + "name": "Samaná" + }, + { + "code": "DO-21", + "name": "San Cristóbal" + }, + { + "code": "DO-31", + "name": "San José de Ocoa" + }, + { + "code": "DO-22", + "name": "San Juan" + }, + { + "code": "DO-23", + "name": "San Pedro de Macorís" + }, + { + "code": "DO-24", + "name": "Sánchez Ramírez" + }, + { + "code": "DO-25", + "name": "Santiago" + }, + { + "code": "DO-26", + "name": "Santiago Rodríguez" + }, + { + "code": "DO-32", + "name": "Santo Domingo" + }, + { + "code": "DO-41", + "name": "Valdesia" + }, + { + "code": "DO-27", + "name": "Valverde" + }, + { + "code": "DO-42", + "name": "Yuma" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EC", + "name": "Ecuador", + "states": [{ + "code": "EC-A", + "name": "Azuay" + }, + { + "code": "EC-B", + "name": "Bolívar" + }, + { + "code": "EC-F", + "name": "Cañar" + }, + { + "code": "EC-C", + "name": "Carchi" + }, + { + "code": "EC-H", + "name": "Chimborazo" + }, + { + "code": "EC-X", + "name": "Cotopaxi" + }, + { + "code": "EC-O", + "name": "El Oro" + }, + { + "code": "EC-E", + "name": "Esmeraldas" + }, + { + "code": "EC-W", + "name": "Galápagos" + }, + { + "code": "EC-G", + "name": "Guayas" + }, + { + "code": "EC-I", + "name": "Imbabura" + }, + { + "code": "EC-L", + "name": "Loja" + }, + { + "code": "EC-R", + "name": "Los Ríos" + }, + { + "code": "EC-M", + "name": "Manabí" + }, + { + "code": "EC-S", + "name": "Morona-Santiago" + }, + { + "code": "EC-N", + "name": "Napo" + }, + { + "code": "EC-D", + "name": "Orellana" + }, + { + "code": "EC-Y", + "name": "Pastaza" + }, + { + "code": "EC-P", + "name": "Pichincha" + }, + { + "code": "EC-SE", + "name": "Santa Elena" + }, + { + "code": "EC-SD", + "name": "Santo Domingo de los Tsáchilas" + }, + { + "code": "EC-U", + "name": "Sucumbíos" + }, + { + "code": "EC-T", + "name": "Tungurahua" + }, + { + "code": "EC-Z", + "name": "Zamora-Chinchipe" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EG", + "name": "Egypt", + "states": [{ + "code": "EGALX", + "name": "Alexandria" + }, + { + "code": "EGASN", + "name": "Aswan" + }, + { + "code": "EGAST", + "name": "Asyut" + }, + { + "code": "EGBA", + "name": "Red Sea" + }, + { + "code": "EGBH", + "name": "Beheira" + }, + { + "code": "EGBNS", + "name": "Beni Suef" + }, + { + "code": "EGC", + "name": "Cairo" + }, + { + "code": "EGDK", + "name": "Dakahlia" + }, + { + "code": "EGDT", + "name": "Damietta" + }, + { + "code": "EGFYM", + "name": "Faiyum" + }, + { + "code": "EGGH", + "name": "Gharbia" + }, + { + "code": "EGGZ", + "name": "Giza" + }, + { + "code": "EGIS", + "name": "Ismailia" + }, + { + "code": "EGJS", + "name": "South Sinai" + }, + { + "code": "EGKB", + "name": "Qalyubia" + }, + { + "code": "EGKFS", + "name": "Kafr el-Sheikh" + }, + { + "code": "EGKN", + "name": "Qena" + }, + { + "code": "EGLX", + "name": "Luxor" + }, + { + "code": "EGMN", + "name": "Minya" + }, + { + "code": "EGMNF", + "name": "Monufia" + }, + { + "code": "EGMT", + "name": "Matrouh" + }, + { + "code": "EGPTS", + "name": "Port Said" + }, + { + "code": "EGSHG", + "name": "Sohag" + }, + { + "code": "EGSHR", + "name": "Al Sharqia" + }, + { + "code": "EGSIN", + "name": "North Sinai" + }, + { + "code": "EGSUZ", + "name": "Suez" + }, + { + "code": "EGWAD", + "name": "New Valley" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SV", + "name": "El Salvador", + "states": [{ + "code": "SV-AH", + "name": "Ahuachapán" + }, + { + "code": "SV-CA", + "name": "Cabañas" + }, + { + "code": "SV-CH", + "name": "Chalatenango" + }, + { + "code": "SV-CU", + "name": "Cuscatlán" + }, + { + "code": "SV-LI", + "name": "La Libertad" + }, + { + "code": "SV-MO", + "name": "Morazán" + }, + { + "code": "SV-PA", + "name": "La Paz" + }, + { + "code": "SV-SA", + "name": "Santa Ana" + }, + { + "code": "SV-SM", + "name": "San Miguel" + }, + { + "code": "SV-SO", + "name": "Sonsonate" + }, + { + "code": "SV-SS", + "name": "San Salvador" + }, + { + "code": "SV-SV", + "name": "San Vicente" + }, + { + "code": "SV-UN", + "name": "La Unión" + }, + { + "code": "SV-US", + "name": "Usulután" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GQ", + "name": "Equatorial Guinea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ER", + "name": "Eritrea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EE", + "name": "Estonia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SZ", + "name": "Eswatini", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ET", + "name": "Ethiopia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FK", + "name": "Falkland Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FO", + "name": "Faroe Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FJ", + "name": "Fiji", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FI", + "name": "Finland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FR", + "name": "France", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GF", + "name": "French Guiana", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PF", + "name": "French Polynesia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TF", + "name": "French Southern Territories", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GA", + "name": "Gabon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GM", + "name": "Gambia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GE", + "name": "Georgia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DE", + "name": "Germany", + "states": [{ + "code": "DE-BW", + "name": "Baden-Württemberg" + }, + { + "code": "DE-BY", + "name": "Bavaria" + }, + { + "code": "DE-BE", + "name": "Berlin" + }, + { + "code": "DE-BB", + "name": "Brandenburg" + }, + { + "code": "DE-HB", + "name": "Bremen" + }, + { + "code": "DE-HH", + "name": "Hamburg" + }, + { + "code": "DE-HE", + "name": "Hesse" + }, + { + "code": "DE-MV", + "name": "Mecklenburg-Vorpommern" + }, + { + "code": "DE-NI", + "name": "Lower Saxony" + }, + { + "code": "DE-NW", + "name": "North Rhine-Westphalia" + }, + { + "code": "DE-RP", + "name": "Rhineland-Palatinate" + }, + { + "code": "DE-SL", + "name": "Saarland" + }, + { + "code": "DE-SN", + "name": "Saxony" + }, + { + "code": "DE-ST", + "name": "Saxony-Anhalt" + }, + { + "code": "DE-SH", + "name": "Schleswig-Holstein" + }, + { + "code": "DE-TH", + "name": "Thuringia" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GH", + "name": "Ghana", + "states": [{ + "code": "AF", + "name": "Ahafo" + }, + { + "code": "AH", + "name": "Ashanti" + }, + { + "code": "BA", + "name": "Brong-Ahafo" + }, + { + "code": "BO", + "name": "Bono" + }, + { + "code": "BE", + "name": "Bono East" + }, + { + "code": "CP", + "name": "Central" + }, + { + "code": "EP", + "name": "Eastern" + }, + { + "code": "AA", + "name": "Greater Accra" + }, + { + "code": "NE", + "name": "North East" + }, + { + "code": "NP", + "name": "Northern" + }, + { + "code": "OT", + "name": "Oti" + }, + { + "code": "SV", + "name": "Savannah" + }, + { + "code": "UE", + "name": "Upper East" + }, + { + "code": "UW", + "name": "Upper West" + }, + { + "code": "TV", + "name": "Volta" + }, + { + "code": "WP", + "name": "Western" + }, + { + "code": "WN", + "name": "Western North" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GI", + "name": "Gibraltar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GR", + "name": "Greece", + "states": [{ + "code": "I", + "name": "Attica" + }, + { + "code": "A", + "name": "East Macedonia and Thrace" + }, + { + "code": "B", + "name": "Central Macedonia" + }, + { + "code": "C", + "name": "West Macedonia" + }, + { + "code": "D", + "name": "Epirus" + }, + { + "code": "E", + "name": "Thessaly" + }, + { + "code": "F", + "name": "Ionian Islands" + }, + { + "code": "G", + "name": "West Greece" + }, + { + "code": "H", + "name": "Central Greece" + }, + { + "code": "J", + "name": "Peloponnese" + }, + { + "code": "K", + "name": "North Aegean" + }, + { + "code": "L", + "name": "South Aegean" + }, + { + "code": "M", + "name": "Crete" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GL", + "name": "Greenland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GD", + "name": "Grenada", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GP", + "name": "Guadeloupe", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GU", + "name": "Guam", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GT", + "name": "Guatemala", + "states": [{ + "code": "GT-AV", + "name": "Alta Verapaz" + }, + { + "code": "GT-BV", + "name": "Baja Verapaz" + }, + { + "code": "GT-CM", + "name": "Chimaltenango" + }, + { + "code": "GT-CQ", + "name": "Chiquimula" + }, + { + "code": "GT-PR", + "name": "El Progreso" + }, + { + "code": "GT-ES", + "name": "Escuintla" + }, + { + "code": "GT-GU", + "name": "Guatemala" + }, + { + "code": "GT-HU", + "name": "Huehuetenango" + }, + { + "code": "GT-IZ", + "name": "Izabal" + }, + { + "code": "GT-JA", + "name": "Jalapa" + }, + { + "code": "GT-JU", + "name": "Jutiapa" + }, + { + "code": "GT-PE", + "name": "Petén" + }, + { + "code": "GT-QZ", + "name": "Quetzaltenango" + }, + { + "code": "GT-QC", + "name": "Quiché" + }, + { + "code": "GT-RE", + "name": "Retalhuleu" + }, + { + "code": "GT-SA", + "name": "Sacatepéquez" + }, + { + "code": "GT-SM", + "name": "San Marcos" + }, + { + "code": "GT-SR", + "name": "Santa Rosa" + }, + { + "code": "GT-SO", + "name": "Sololá" + }, + { + "code": "GT-SU", + "name": "Suchitepéquez" + }, + { + "code": "GT-TO", + "name": "Totonicapán" + }, + { + "code": "GT-ZA", + "name": "Zacapa" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GG", + "name": "Guernsey", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GN", + "name": "Guinea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GW", + "name": "Guinea-Bissau", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GY", + "name": "Guyana", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HT", + "name": "Haiti", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HM", + "name": "Heard Island and McDonald Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HN", + "name": "Honduras", + "states": [{ + "code": "HN-AT", + "name": "Atlántida" + }, + { + "code": "HN-IB", + "name": "Bay Islands" + }, + { + "code": "HN-CH", + "name": "Choluteca" + }, + { + "code": "HN-CL", + "name": "Colón" + }, + { + "code": "HN-CM", + "name": "Comayagua" + }, + { + "code": "HN-CP", + "name": "Copán" + }, + { + "code": "HN-CR", + "name": "Cortés" + }, + { + "code": "HN-EP", + "name": "El Paraíso" + }, + { + "code": "HN-FM", + "name": "Francisco Morazán" + }, + { + "code": "HN-GD", + "name": "Gracias a Dios" + }, + { + "code": "HN-IN", + "name": "Intibucá" + }, + { + "code": "HN-LE", + "name": "Lempira" + }, + { + "code": "HN-LP", + "name": "La Paz" + }, + { + "code": "HN-OC", + "name": "Ocotepeque" + }, + { + "code": "HN-OL", + "name": "Olancho" + }, + { + "code": "HN-SB", + "name": "Santa Bárbara" + }, + { + "code": "HN-VA", + "name": "Valle" + }, + { + "code": "HN-YO", + "name": "Yoro" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HK", + "name": "Hong Kong", + "states": [{ + "code": "HONG KONG", + "name": "Hong Kong Island" + }, + { + "code": "KOWLOON", + "name": "Kowloon" + }, + { + "code": "NEW TERRITORIES", + "name": "New Territories" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HU", + "name": "Hungary", + "states": [{ + "code": "BK", + "name": "Bács-Kiskun" + }, + { + "code": "BE", + "name": "Békés" + }, + { + "code": "BA", + "name": "Baranya" + }, + { + "code": "BZ", + "name": "Borsod-Abaúj-Zemplén" + }, + { + "code": "BU", + "name": "Budapest" + }, + { + "code": "CS", + "name": "Csongrád-Csanád" + }, + { + "code": "FE", + "name": "Fejér" + }, + { + "code": "GS", + "name": "Győr-Moson-Sopron" + }, + { + "code": "HB", + "name": "Hajdú-Bihar" + }, + { + "code": "HE", + "name": "Heves" + }, + { + "code": "JN", + "name": "Jász-Nagykun-Szolnok" + }, + { + "code": "KE", + "name": "Komárom-Esztergom" + }, + { + "code": "NO", + "name": "Nógrád" + }, + { + "code": "PE", + "name": "Pest" + }, + { + "code": "SO", + "name": "Somogy" + }, + { + "code": "SZ", + "name": "Szabolcs-Szatmár-Bereg" + }, + { + "code": "TO", + "name": "Tolna" + }, + { + "code": "VA", + "name": "Vas" + }, + { + "code": "VE", + "name": "Veszprém" + }, + { + "code": "ZA", + "name": "Zala" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IS", + "name": "Iceland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IN", + "name": "India", + "states": [{ + "code": "AP", + "name": "Andhra Pradesh" + }, + { + "code": "AR", + "name": "Arunachal Pradesh" + }, + { + "code": "AS", + "name": "Assam" + }, + { + "code": "BR", + "name": "Bihar" + }, + { + "code": "CT", + "name": "Chhattisgarh" + }, + { + "code": "GA", + "name": "Goa" + }, + { + "code": "GJ", + "name": "Gujarat" + }, + { + "code": "HR", + "name": "Haryana" + }, + { + "code": "HP", + "name": "Himachal Pradesh" + }, + { + "code": "JK", + "name": "Jammu and Kashmir" + }, + { + "code": "JH", + "name": "Jharkhand" + }, + { + "code": "KA", + "name": "Karnataka" + }, + { + "code": "KL", + "name": "Kerala" + }, + { + "code": "LA", + "name": "Ladakh" + }, + { + "code": "MP", + "name": "Madhya Pradesh" + }, + { + "code": "MH", + "name": "Maharashtra" + }, + { + "code": "MN", + "name": "Manipur" + }, + { + "code": "ML", + "name": "Meghalaya" + }, + { + "code": "MZ", + "name": "Mizoram" + }, + { + "code": "NL", + "name": "Nagaland" + }, + { + "code": "OR", + "name": "Odisha" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "RJ", + "name": "Rajasthan" + }, + { + "code": "SK", + "name": "Sikkim" + }, + { + "code": "TN", + "name": "Tamil Nadu" + }, + { + "code": "TS", + "name": "Telangana" + }, + { + "code": "TR", + "name": "Tripura" + }, + { + "code": "UK", + "name": "Uttarakhand" + }, + { + "code": "UP", + "name": "Uttar Pradesh" + }, + { + "code": "WB", + "name": "West Bengal" + }, + { + "code": "AN", + "name": "Andaman and Nicobar Islands" + }, + { + "code": "CH", + "name": "Chandigarh" + }, + { + "code": "DN", + "name": "Dadra and Nagar Haveli" + }, + { + "code": "DD", + "name": "Daman and Diu" + }, + { + "code": "DL", + "name": "Delhi" + }, + { + "code": "LD", + "name": "Lakshadeep" + }, + { + "code": "PY", + "name": "Pondicherry (Puducherry)" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ID", + "name": "Indonesia", + "states": [{ + "code": "AC", + "name": "Daerah Istimewa Aceh" + }, + { + "code": "SU", + "name": "Sumatera Utara" + }, + { + "code": "SB", + "name": "Sumatera Barat" + }, + { + "code": "RI", + "name": "Riau" + }, + { + "code": "KR", + "name": "Kepulauan Riau" + }, + { + "code": "JA", + "name": "Jambi" + }, + { + "code": "SS", + "name": "Sumatera Selatan" + }, + { + "code": "BB", + "name": "Bangka Belitung" + }, + { + "code": "BE", + "name": "Bengkulu" + }, + { + "code": "LA", + "name": "Lampung" + }, + { + "code": "JK", + "name": "DKI Jakarta" + }, + { + "code": "JB", + "name": "Jawa Barat" + }, + { + "code": "BT", + "name": "Banten" + }, + { + "code": "JT", + "name": "Jawa Tengah" + }, + { + "code": "JI", + "name": "Jawa Timur" + }, + { + "code": "YO", + "name": "Daerah Istimewa Yogyakarta" + }, + { + "code": "BA", + "name": "Bali" + }, + { + "code": "NB", + "name": "Nusa Tenggara Barat" + }, + { + "code": "NT", + "name": "Nusa Tenggara Timur" + }, + { + "code": "KB", + "name": "Kalimantan Barat" + }, + { + "code": "KT", + "name": "Kalimantan Tengah" + }, + { + "code": "KI", + "name": "Kalimantan Timur" + }, + { + "code": "KS", + "name": "Kalimantan Selatan" + }, + { + "code": "KU", + "name": "Kalimantan Utara" + }, + { + "code": "SA", + "name": "Sulawesi Utara" + }, + { + "code": "ST", + "name": "Sulawesi Tengah" + }, + { + "code": "SG", + "name": "Sulawesi Tenggara" + }, + { + "code": "SR", + "name": "Sulawesi Barat" + }, + { + "code": "SN", + "name": "Sulawesi Selatan" + }, + { + "code": "GO", + "name": "Gorontalo" + }, + { + "code": "MA", + "name": "Maluku" + }, + { + "code": "MU", + "name": "Maluku Utara" + }, + { + "code": "PA", + "name": "Papua" + }, + { + "code": "PB", + "name": "Papua Barat" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IR", + "name": "Iran", + "states": [{ + "code": "KHZ", + "name": "Khuzestan (خوزستان)" + }, + { + "code": "THR", + "name": "Tehran (تهران)" + }, + { + "code": "ILM", + "name": "Ilaam (ایلام)" + }, + { + "code": "BHR", + "name": "Bushehr (بوشهر)" + }, + { + "code": "ADL", + "name": "Ardabil (اردبیل)" + }, + { + "code": "ESF", + "name": "Isfahan (اصفهان)" + }, + { + "code": "YZD", + "name": "Yazd (یزد)" + }, + { + "code": "KRH", + "name": "Kermanshah (کرمانشاه)" + }, + { + "code": "KRN", + "name": "Kerman (کرمان)" + }, + { + "code": "HDN", + "name": "Hamadan (همدان)" + }, + { + "code": "GZN", + "name": "Ghazvin (قزوین)" + }, + { + "code": "ZJN", + "name": "Zanjan (زنجان)" + }, + { + "code": "LRS", + "name": "Luristan (لرستان)" + }, + { + "code": "ABZ", + "name": "Alborz (البرز)" + }, + { + "code": "EAZ", + "name": "East Azarbaijan (آذربایجان شرقی)" + }, + { + "code": "WAZ", + "name": "West Azarbaijan (آذربایجان غربی)" + }, + { + "code": "CHB", + "name": "Chaharmahal and Bakhtiari (چهارمحال و بختیاری)" + }, + { + "code": "SKH", + "name": "South Khorasan (خراسان جنوبی)" + }, + { + "code": "RKH", + "name": "Razavi Khorasan (خراسان رضوی)" + }, + { + "code": "NKH", + "name": "North Khorasan (خراسان شمالی)" + }, + { + "code": "SMN", + "name": "Semnan (سمنان)" + }, + { + "code": "FRS", + "name": "Fars (فارس)" + }, + { + "code": "QHM", + "name": "Qom (قم)" + }, + { + "code": "KRD", + "name": "Kurdistan / کردستان)" + }, + { + "code": "KBD", + "name": "Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)" + }, + { + "code": "GLS", + "name": "Golestan (گلستان)" + }, + { + "code": "GIL", + "name": "Gilan (گیلان)" + }, + { + "code": "MZN", + "name": "Mazandaran (مازندران)" + }, + { + "code": "MKZ", + "name": "Markazi (مرکزی)" + }, + { + "code": "HRZ", + "name": "Hormozgan (هرمزگان)" + }, + { + "code": "SBN", + "name": "Sistan and Baluchestan (سیستان و بلوچستان)" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IQ", + "name": "Iraq", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IE", + "name": "Ireland", + "states": [{ + "code": "CW", + "name": "Carlow" + }, + { + "code": "CN", + "name": "Cavan" + }, + { + "code": "CE", + "name": "Clare" + }, + { + "code": "CO", + "name": "Cork" + }, + { + "code": "DL", + "name": "Donegal" + }, + { + "code": "D", + "name": "Dublin" + }, + { + "code": "G", + "name": "Galway" + }, + { + "code": "KY", + "name": "Kerry" + }, + { + "code": "KE", + "name": "Kildare" + }, + { + "code": "KK", + "name": "Kilkenny" + }, + { + "code": "LS", + "name": "Laois" + }, + { + "code": "LM", + "name": "Leitrim" + }, + { + "code": "LK", + "name": "Limerick" + }, + { + "code": "LD", + "name": "Longford" + }, + { + "code": "LH", + "name": "Louth" + }, + { + "code": "MO", + "name": "Mayo" + }, + { + "code": "MH", + "name": "Meath" + }, + { + "code": "MN", + "name": "Monaghan" + }, + { + "code": "OY", + "name": "Offaly" + }, + { + "code": "RN", + "name": "Roscommon" + }, + { + "code": "SO", + "name": "Sligo" + }, + { + "code": "TA", + "name": "Tipperary" + }, + { + "code": "WD", + "name": "Waterford" + }, + { + "code": "WH", + "name": "Westmeath" + }, + { + "code": "WX", + "name": "Wexford" + }, + { + "code": "WW", + "name": "Wicklow" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IM", + "name": "Isle of Man", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IL", + "name": "Israel", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IT", + "name": "Italy", + "states": [{ + "code": "AG", + "name": "Agrigento" + }, + { + "code": "AL", + "name": "Alessandria" + }, + { + "code": "AN", + "name": "Ancona" + }, + { + "code": "AO", + "name": "Aosta" + }, + { + "code": "AR", + "name": "Arezzo" + }, + { + "code": "AP", + "name": "Ascoli Piceno" + }, + { + "code": "AT", + "name": "Asti" + }, + { + "code": "AV", + "name": "Avellino" + }, + { + "code": "BA", + "name": "Bari" + }, + { + "code": "BT", + "name": "Barletta-Andria-Trani" + }, + { + "code": "BL", + "name": "Belluno" + }, + { + "code": "BN", + "name": "Benevento" + }, + { + "code": "BG", + "name": "Bergamo" + }, + { + "code": "BI", + "name": "Biella" + }, + { + "code": "BO", + "name": "Bologna" + }, + { + "code": "BZ", + "name": "Bolzano" + }, + { + "code": "BS", + "name": "Brescia" + }, + { + "code": "BR", + "name": "Brindisi" + }, + { + "code": "CA", + "name": "Cagliari" + }, + { + "code": "CL", + "name": "Caltanissetta" + }, + { + "code": "CB", + "name": "Campobasso" + }, + { + "code": "CE", + "name": "Caserta" + }, + { + "code": "CT", + "name": "Catania" + }, + { + "code": "CZ", + "name": "Catanzaro" + }, + { + "code": "CH", + "name": "Chieti" + }, + { + "code": "CO", + "name": "Como" + }, + { + "code": "CS", + "name": "Cosenza" + }, + { + "code": "CR", + "name": "Cremona" + }, + { + "code": "KR", + "name": "Crotone" + }, + { + "code": "CN", + "name": "Cuneo" + }, + { + "code": "EN", + "name": "Enna" + }, + { + "code": "FM", + "name": "Fermo" + }, + { + "code": "FE", + "name": "Ferrara" + }, + { + "code": "FI", + "name": "Firenze" + }, + { + "code": "FG", + "name": "Foggia" + }, + { + "code": "FC", + "name": "Forlì-Cesena" + }, + { + "code": "FR", + "name": "Frosinone" + }, + { + "code": "GE", + "name": "Genova" + }, + { + "code": "GO", + "name": "Gorizia" + }, + { + "code": "GR", + "name": "Grosseto" + }, + { + "code": "IM", + "name": "Imperia" + }, + { + "code": "IS", + "name": "Isernia" + }, + { + "code": "SP", + "name": "La Spezia" + }, + { + "code": "AQ", + "name": "L'Aquila" + }, + { + "code": "LT", + "name": "Latina" + }, + { + "code": "LE", + "name": "Lecce" + }, + { + "code": "LC", + "name": "Lecco" + }, + { + "code": "LI", + "name": "Livorno" + }, + { + "code": "LO", + "name": "Lodi" + }, + { + "code": "LU", + "name": "Lucca" + }, + { + "code": "MC", + "name": "Macerata" + }, + { + "code": "MN", + "name": "Mantova" + }, + { + "code": "MS", + "name": "Massa-Carrara" + }, + { + "code": "MT", + "name": "Matera" + }, + { + "code": "ME", + "name": "Messina" + }, + { + "code": "MI", + "name": "Milano" + }, + { + "code": "MO", + "name": "Modena" + }, + { + "code": "MB", + "name": "Monza e della Brianza" + }, + { + "code": "NA", + "name": "Napoli" + }, + { + "code": "NO", + "name": "Novara" + }, + { + "code": "NU", + "name": "Nuoro" + }, + { + "code": "OR", + "name": "Oristano" + }, + { + "code": "PD", + "name": "Padova" + }, + { + "code": "PA", + "name": "Palermo" + }, + { + "code": "PR", + "name": "Parma" + }, + { + "code": "PV", + "name": "Pavia" + }, + { + "code": "PG", + "name": "Perugia" + }, + { + "code": "PU", + "name": "Pesaro e Urbino" + }, + { + "code": "PE", + "name": "Pescara" + }, + { + "code": "PC", + "name": "Piacenza" + }, + { + "code": "PI", + "name": "Pisa" + }, + { + "code": "PT", + "name": "Pistoia" + }, + { + "code": "PN", + "name": "Pordenone" + }, + { + "code": "PZ", + "name": "Potenza" + }, + { + "code": "PO", + "name": "Prato" + }, + { + "code": "RG", + "name": "Ragusa" + }, + { + "code": "RA", + "name": "Ravenna" + }, + { + "code": "RC", + "name": "Reggio Calabria" + }, + { + "code": "RE", + "name": "Reggio Emilia" + }, + { + "code": "RI", + "name": "Rieti" + }, + { + "code": "RN", + "name": "Rimini" + }, + { + "code": "RM", + "name": "Roma" + }, + { + "code": "RO", + "name": "Rovigo" + }, + { + "code": "SA", + "name": "Salerno" + }, + { + "code": "SS", + "name": "Sassari" + }, + { + "code": "SV", + "name": "Savona" + }, + { + "code": "SI", + "name": "Siena" + }, + { + "code": "SR", + "name": "Siracusa" + }, + { + "code": "SO", + "name": "Sondrio" + }, + { + "code": "SU", + "name": "Sud Sardegna" + }, + { + "code": "TA", + "name": "Taranto" + }, + { + "code": "TE", + "name": "Teramo" + }, + { + "code": "TR", + "name": "Terni" + }, + { + "code": "TO", + "name": "Torino" + }, + { + "code": "TP", + "name": "Trapani" + }, + { + "code": "TN", + "name": "Trento" + }, + { + "code": "TV", + "name": "Treviso" + }, + { + "code": "TS", + "name": "Trieste" + }, + { + "code": "UD", + "name": "Udine" + }, + { + "code": "VA", + "name": "Varese" + }, + { + "code": "VE", + "name": "Venezia" + }, + { + "code": "VB", + "name": "Verbano-Cusio-Ossola" + }, + { + "code": "VC", + "name": "Vercelli" + }, + { + "code": "VR", + "name": "Verona" + }, + { + "code": "VV", + "name": "Vibo Valentia" + }, + { + "code": "VI", + "name": "Vicenza" + }, + { + "code": "VT", + "name": "Viterbo" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CI", + "name": "Ivory Coast", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JM", + "name": "Jamaica", + "states": [{ + "code": "JM-01", + "name": "Kingston" + }, + { + "code": "JM-02", + "name": "Saint Andrew" + }, + { + "code": "JM-03", + "name": "Saint Thomas" + }, + { + "code": "JM-04", + "name": "Portland" + }, + { + "code": "JM-05", + "name": "Saint Mary" + }, + { + "code": "JM-06", + "name": "Saint Ann" + }, + { + "code": "JM-07", + "name": "Trelawny" + }, + { + "code": "JM-08", + "name": "Saint James" + }, + { + "code": "JM-09", + "name": "Hanover" + }, + { + "code": "JM-10", + "name": "Westmoreland" + }, + { + "code": "JM-11", + "name": "Saint Elizabeth" + }, + { + "code": "JM-12", + "name": "Manchester" + }, + { + "code": "JM-13", + "name": "Clarendon" + }, + { + "code": "JM-14", + "name": "Saint Catherine" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JP", + "name": "Japan", + "states": [{ + "code": "JP01", + "name": "Hokkaido" + }, + { + "code": "JP02", + "name": "Aomori" + }, + { + "code": "JP03", + "name": "Iwate" + }, + { + "code": "JP04", + "name": "Miyagi" + }, + { + "code": "JP05", + "name": "Akita" + }, + { + "code": "JP06", + "name": "Yamagata" + }, + { + "code": "JP07", + "name": "Fukushima" + }, + { + "code": "JP08", + "name": "Ibaraki" + }, + { + "code": "JP09", + "name": "Tochigi" + }, + { + "code": "JP10", + "name": "Gunma" + }, + { + "code": "JP11", + "name": "Saitama" + }, + { + "code": "JP12", + "name": "Chiba" + }, + { + "code": "JP13", + "name": "Tokyo" + }, + { + "code": "JP14", + "name": "Kanagawa" + }, + { + "code": "JP15", + "name": "Niigata" + }, + { + "code": "JP16", + "name": "Toyama" + }, + { + "code": "JP17", + "name": "Ishikawa" + }, + { + "code": "JP18", + "name": "Fukui" + }, + { + "code": "JP19", + "name": "Yamanashi" + }, + { + "code": "JP20", + "name": "Nagano" + }, + { + "code": "JP21", + "name": "Gifu" + }, + { + "code": "JP22", + "name": "Shizuoka" + }, + { + "code": "JP23", + "name": "Aichi" + }, + { + "code": "JP24", + "name": "Mie" + }, + { + "code": "JP25", + "name": "Shiga" + }, + { + "code": "JP26", + "name": "Kyoto" + }, + { + "code": "JP27", + "name": "Osaka" + }, + { + "code": "JP28", + "name": "Hyogo" + }, + { + "code": "JP29", + "name": "Nara" + }, + { + "code": "JP30", + "name": "Wakayama" + }, + { + "code": "JP31", + "name": "Tottori" + }, + { + "code": "JP32", + "name": "Shimane" + }, + { + "code": "JP33", + "name": "Okayama" + }, + { + "code": "JP34", + "name": "Hiroshima" + }, + { + "code": "JP35", + "name": "Yamaguchi" + }, + { + "code": "JP36", + "name": "Tokushima" + }, + { + "code": "JP37", + "name": "Kagawa" + }, + { + "code": "JP38", + "name": "Ehime" + }, + { + "code": "JP39", + "name": "Kochi" + }, + { + "code": "JP40", + "name": "Fukuoka" + }, + { + "code": "JP41", + "name": "Saga" + }, + { + "code": "JP42", + "name": "Nagasaki" + }, + { + "code": "JP43", + "name": "Kumamoto" + }, + { + "code": "JP44", + "name": "Oita" + }, + { + "code": "JP45", + "name": "Miyazaki" + }, + { + "code": "JP46", + "name": "Kagoshima" + }, + { + "code": "JP47", + "name": "Okinawa" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JE", + "name": "Jersey", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JO", + "name": "Jordan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KZ", + "name": "Kazakhstan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KE", + "name": "Kenya", + "states": [{ + "code": "KE01", + "name": "Baringo" + }, + { + "code": "KE02", + "name": "Bomet" + }, + { + "code": "KE03", + "name": "Bungoma" + }, + { + "code": "KE04", + "name": "Busia" + }, + { + "code": "KE05", + "name": "Elgeyo-Marakwet" + }, + { + "code": "KE06", + "name": "Embu" + }, + { + "code": "KE07", + "name": "Garissa" + }, + { + "code": "KE08", + "name": "Homa Bay" + }, + { + "code": "KE09", + "name": "Isiolo" + }, + { + "code": "KE10", + "name": "Kajiado" + }, + { + "code": "KE11", + "name": "Kakamega" + }, + { + "code": "KE12", + "name": "Kericho" + }, + { + "code": "KE13", + "name": "Kiambu" + }, + { + "code": "KE14", + "name": "Kilifi" + }, + { + "code": "KE15", + "name": "Kirinyaga" + }, + { + "code": "KE16", + "name": "Kisii" + }, + { + "code": "KE17", + "name": "Kisumu" + }, + { + "code": "KE18", + "name": "Kitui" + }, + { + "code": "KE19", + "name": "Kwale" + }, + { + "code": "KE20", + "name": "Laikipia" + }, + { + "code": "KE21", + "name": "Lamu" + }, + { + "code": "KE22", + "name": "Machakos" + }, + { + "code": "KE23", + "name": "Makueni" + }, + { + "code": "KE24", + "name": "Mandera" + }, + { + "code": "KE25", + "name": "Marsabit" + }, + { + "code": "KE26", + "name": "Meru" + }, + { + "code": "KE27", + "name": "Migori" + }, + { + "code": "KE28", + "name": "Mombasa" + }, + { + "code": "KE29", + "name": "Murang’a" + }, + { + "code": "KE30", + "name": "Nairobi County" + }, + { + "code": "KE31", + "name": "Nakuru" + }, + { + "code": "KE32", + "name": "Nandi" + }, + { + "code": "KE33", + "name": "Narok" + }, + { + "code": "KE34", + "name": "Nyamira" + }, + { + "code": "KE35", + "name": "Nyandarua" + }, + { + "code": "KE36", + "name": "Nyeri" + }, + { + "code": "KE37", + "name": "Samburu" + }, + { + "code": "KE38", + "name": "Siaya" + }, + { + "code": "KE39", + "name": "Taita-Taveta" + }, + { + "code": "KE40", + "name": "Tana River" + }, + { + "code": "KE41", + "name": "Tharaka-Nithi" + }, + { + "code": "KE42", + "name": "Trans Nzoia" + }, + { + "code": "KE43", + "name": "Turkana" + }, + { + "code": "KE44", + "name": "Uasin Gishu" + }, + { + "code": "KE45", + "name": "Vihiga" + }, + { + "code": "KE46", + "name": "Wajir" + }, + { + "code": "KE47", + "name": "West Pokot" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KI", + "name": "Kiribati", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KW", + "name": "Kuwait", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KG", + "name": "Kyrgyzstan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LA", + "name": "Laos", + "states": [{ + "code": "AT", + "name": "Attapeu" + }, + { + "code": "BK", + "name": "Bokeo" + }, + { + "code": "BL", + "name": "Bolikhamsai" + }, + { + "code": "CH", + "name": "Champasak" + }, + { + "code": "HO", + "name": "Houaphanh" + }, + { + "code": "KH", + "name": "Khammouane" + }, + { + "code": "LM", + "name": "Luang Namtha" + }, + { + "code": "LP", + "name": "Luang Prabang" + }, + { + "code": "OU", + "name": "Oudomxay" + }, + { + "code": "PH", + "name": "Phongsaly" + }, + { + "code": "SL", + "name": "Salavan" + }, + { + "code": "SV", + "name": "Savannakhet" + }, + { + "code": "VI", + "name": "Vientiane Province" + }, + { + "code": "VT", + "name": "Vientiane" + }, + { + "code": "XA", + "name": "Sainyabuli" + }, + { + "code": "XE", + "name": "Sekong" + }, + { + "code": "XI", + "name": "Xiangkhouang" + }, + { + "code": "XS", + "name": "Xaisomboun" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LV", + "name": "Latvia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LB", + "name": "Lebanon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LS", + "name": "Lesotho", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LR", + "name": "Liberia", + "states": [{ + "code": "BM", + "name": "Bomi" + }, + { + "code": "BN", + "name": "Bong" + }, + { + "code": "GA", + "name": "Gbarpolu" + }, + { + "code": "GB", + "name": "Grand Bassa" + }, + { + "code": "GC", + "name": "Grand Cape Mount" + }, + { + "code": "GG", + "name": "Grand Gedeh" + }, + { + "code": "GK", + "name": "Grand Kru" + }, + { + "code": "LO", + "name": "Lofa" + }, + { + "code": "MA", + "name": "Margibi" + }, + { + "code": "MY", + "name": "Maryland" + }, + { + "code": "MO", + "name": "Montserrado" + }, + { + "code": "NM", + "name": "Nimba" + }, + { + "code": "RV", + "name": "Rivercess" + }, + { + "code": "RG", + "name": "River Gee" + }, + { + "code": "SN", + "name": "Sinoe" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LY", + "name": "Libya", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LI", + "name": "Liechtenstein", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LT", + "name": "Lithuania", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LU", + "name": "Luxembourg", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MO", + "name": "Macao", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MG", + "name": "Madagascar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MW", + "name": "Malawi", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MY", + "name": "Malaysia", + "states": [{ + "code": "JHR", + "name": "Johor" + }, + { + "code": "KDH", + "name": "Kedah" + }, + { + "code": "KTN", + "name": "Kelantan" + }, + { + "code": "LBN", + "name": "Labuan" + }, + { + "code": "MLK", + "name": "Malacca (Melaka)" + }, + { + "code": "NSN", + "name": "Negeri Sembilan" + }, + { + "code": "PHG", + "name": "Pahang" + }, + { + "code": "PNG", + "name": "Penang (Pulau Pinang)" + }, + { + "code": "PRK", + "name": "Perak" + }, + { + "code": "PLS", + "name": "Perlis" + }, + { + "code": "SBH", + "name": "Sabah" + }, + { + "code": "SWK", + "name": "Sarawak" + }, + { + "code": "SGR", + "name": "Selangor" + }, + { + "code": "TRG", + "name": "Terengganu" + }, + { + "code": "PJY", + "name": "Putrajaya" + }, + { + "code": "KUL", + "name": "Kuala Lumpur" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MV", + "name": "Maldives", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ML", + "name": "Mali", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MT", + "name": "Malta", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MH", + "name": "Marshall Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MQ", + "name": "Martinique", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MR", + "name": "Mauritania", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MU", + "name": "Mauritius", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "YT", + "name": "Mayotte", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MX", + "name": "Mexico", + "states": [{ + "code": "DF", + "name": "Ciudad de México" + }, + { + "code": "JA", + "name": "Jalisco" + }, + { + "code": "NL", + "name": "Nuevo León" + }, + { + "code": "AG", + "name": "Aguascalientes" + }, + { + "code": "BC", + "name": "Baja California" + }, + { + "code": "BS", + "name": "Baja California Sur" + }, + { + "code": "CM", + "name": "Campeche" + }, + { + "code": "CS", + "name": "Chiapas" + }, + { + "code": "CH", + "name": "Chihuahua" + }, + { + "code": "CO", + "name": "Coahuila" + }, + { + "code": "CL", + "name": "Colima" + }, + { + "code": "DG", + "name": "Durango" + }, + { + "code": "GT", + "name": "Guanajuato" + }, + { + "code": "GR", + "name": "Guerrero" + }, + { + "code": "HG", + "name": "Hidalgo" + }, + { + "code": "MX", + "name": "Estado de México" + }, + { + "code": "MI", + "name": "Michoacán" + }, + { + "code": "MO", + "name": "Morelos" + }, + { + "code": "NA", + "name": "Nayarit" + }, + { + "code": "OA", + "name": "Oaxaca" + }, + { + "code": "PU", + "name": "Puebla" + }, + { + "code": "QT", + "name": "Querétaro" + }, + { + "code": "QR", + "name": "Quintana Roo" + }, + { + "code": "SL", + "name": "San Luis Potosí" + }, + { + "code": "SI", + "name": "Sinaloa" + }, + { + "code": "SO", + "name": "Sonora" + }, + { + "code": "TB", + "name": "Tabasco" + }, + { + "code": "TM", + "name": "Tamaulipas" + }, + { + "code": "TL", + "name": "Tlaxcala" + }, + { + "code": "VE", + "name": "Veracruz" + }, + { + "code": "YU", + "name": "Yucatán" + }, + { + "code": "ZA", + "name": "Zacatecas" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FM", + "name": "Micronesia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MD", + "name": "Moldova", + "states": [{ + "code": "C", + "name": "Chișinău" + }, + { + "code": "BL", + "name": "Bălți" + }, + { + "code": "AN", + "name": "Anenii Noi" + }, + { + "code": "BS", + "name": "Basarabeasca" + }, + { + "code": "BR", + "name": "Briceni" + }, + { + "code": "CH", + "name": "Cahul" + }, + { + "code": "CT", + "name": "Cantemir" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Căușeni" + }, + { + "code": "CM", + "name": "Cimișlia" + }, + { + "code": "CR", + "name": "Criuleni" + }, + { + "code": "DN", + "name": "Dondușeni" + }, + { + "code": "DR", + "name": "Drochia" + }, + { + "code": "DB", + "name": "Dubăsari" + }, + { + "code": "ED", + "name": "Edineț" + }, + { + "code": "FL", + "name": "Fălești" + }, + { + "code": "FR", + "name": "Florești" + }, + { + "code": "GE", + "name": "UTA Găgăuzia" + }, + { + "code": "GL", + "name": "Glodeni" + }, + { + "code": "HN", + "name": "Hîncești" + }, + { + "code": "IL", + "name": "Ialoveni" + }, + { + "code": "LV", + "name": "Leova" + }, + { + "code": "NS", + "name": "Nisporeni" + }, + { + "code": "OC", + "name": "Ocnița" + }, + { + "code": "OR", + "name": "Orhei" + }, + { + "code": "RZ", + "name": "Rezina" + }, + { + "code": "RS", + "name": "Rîșcani" + }, + { + "code": "SG", + "name": "Sîngerei" + }, + { + "code": "SR", + "name": "Soroca" + }, + { + "code": "ST", + "name": "Strășeni" + }, + { + "code": "SD", + "name": "Șoldănești" + }, + { + "code": "SV", + "name": "Ștefan Vodă" + }, + { + "code": "TR", + "name": "Taraclia" + }, + { + "code": "TL", + "name": "Telenești" + }, + { + "code": "UN", + "name": "Ungheni" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MC", + "name": "Monaco", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MN", + "name": "Mongolia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ME", + "name": "Montenegro", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MS", + "name": "Montserrat", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MA", + "name": "Morocco", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MZ", + "name": "Mozambique", + "states": [{ + "code": "MZP", + "name": "Cabo Delgado" + }, + { + "code": "MZG", + "name": "Gaza" + }, + { + "code": "MZI", + "name": "Inhambane" + }, + { + "code": "MZB", + "name": "Manica" + }, + { + "code": "MZL", + "name": "Maputo Province" + }, + { + "code": "MZMPM", + "name": "Maputo" + }, + { + "code": "MZN", + "name": "Nampula" + }, + { + "code": "MZA", + "name": "Niassa" + }, + { + "code": "MZS", + "name": "Sofala" + }, + { + "code": "MZT", + "name": "Tete" + }, + { + "code": "MZQ", + "name": "Zambézia" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MM", + "name": "Myanmar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NA", + "name": "Namibia", + "states": [{ + "code": "ER", + "name": "Erongo" + }, + { + "code": "HA", + "name": "Hardap" + }, + { + "code": "KA", + "name": "Karas" + }, + { + "code": "KE", + "name": "Kavango East" + }, + { + "code": "KW", + "name": "Kavango West" + }, + { + "code": "KH", + "name": "Khomas" + }, + { + "code": "KU", + "name": "Kunene" + }, + { + "code": "OW", + "name": "Ohangwena" + }, + { + "code": "OH", + "name": "Omaheke" + }, + { + "code": "OS", + "name": "Omusati" + }, + { + "code": "ON", + "name": "Oshana" + }, + { + "code": "OT", + "name": "Oshikoto" + }, + { + "code": "OD", + "name": "Otjozondjupa" + }, + { + "code": "CA", + "name": "Zambezi" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NR", + "name": "Nauru", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NP", + "name": "Nepal", + "states": [{ + "code": "BAG", + "name": "Bagmati" + }, + { + "code": "BHE", + "name": "Bheri" + }, + { + "code": "DHA", + "name": "Dhaulagiri" + }, + { + "code": "GAN", + "name": "Gandaki" + }, + { + "code": "JAN", + "name": "Janakpur" + }, + { + "code": "KAR", + "name": "Karnali" + }, + { + "code": "KOS", + "name": "Koshi" + }, + { + "code": "LUM", + "name": "Lumbini" + }, + { + "code": "MAH", + "name": "Mahakali" + }, + { + "code": "MEC", + "name": "Mechi" + }, + { + "code": "NAR", + "name": "Narayani" + }, + { + "code": "RAP", + "name": "Rapti" + }, + { + "code": "SAG", + "name": "Sagarmatha" + }, + { + "code": "SET", + "name": "Seti" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NL", + "name": "Netherlands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NC", + "name": "New Caledonia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NZ", + "name": "New Zealand", + "states": [{ + "code": "NTL", + "name": "Northland" + }, + { + "code": "AUK", + "name": "Auckland" + }, + { + "code": "WKO", + "name": "Waikato" + }, + { + "code": "BOP", + "name": "Bay of Plenty" + }, + { + "code": "TKI", + "name": "Taranaki" + }, + { + "code": "GIS", + "name": "Gisborne" + }, + { + "code": "HKB", + "name": "Hawke’s Bay" + }, + { + "code": "MWT", + "name": "Manawatu-Wanganui" + }, + { + "code": "WGN", + "name": "Wellington" + }, + { + "code": "NSN", + "name": "Nelson" + }, + { + "code": "MBH", + "name": "Marlborough" + }, + { + "code": "TAS", + "name": "Tasman" + }, + { + "code": "WTC", + "name": "West Coast" + }, + { + "code": "CAN", + "name": "Canterbury" + }, + { + "code": "OTA", + "name": "Otago" + }, + { + "code": "STL", + "name": "Southland" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NI", + "name": "Nicaragua", + "states": [{ + "code": "NI-AN", + "name": "Atlántico Norte" + }, + { + "code": "NI-AS", + "name": "Atlántico Sur" + }, + { + "code": "NI-BO", + "name": "Boaco" + }, + { + "code": "NI-CA", + "name": "Carazo" + }, + { + "code": "NI-CI", + "name": "Chinandega" + }, + { + "code": "NI-CO", + "name": "Chontales" + }, + { + "code": "NI-ES", + "name": "Estelí" + }, + { + "code": "NI-GR", + "name": "Granada" + }, + { + "code": "NI-JI", + "name": "Jinotega" + }, + { + "code": "NI-LE", + "name": "León" + }, + { + "code": "NI-MD", + "name": "Madriz" + }, + { + "code": "NI-MN", + "name": "Managua" + }, + { + "code": "NI-MS", + "name": "Masaya" + }, + { + "code": "NI-MT", + "name": "Matagalpa" + }, + { + "code": "NI-NS", + "name": "Nueva Segovia" + }, + { + "code": "NI-RI", + "name": "Rivas" + }, + { + "code": "NI-SJ", + "name": "Río San Juan" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NE", + "name": "Niger", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NG", + "name": "Nigeria", + "states": [{ + "code": "AB", + "name": "Abia" + }, + { + "code": "FC", + "name": "Abuja" + }, + { + "code": "AD", + "name": "Adamawa" + }, + { + "code": "AK", + "name": "Akwa Ibom" + }, + { + "code": "AN", + "name": "Anambra" + }, + { + "code": "BA", + "name": "Bauchi" + }, + { + "code": "BY", + "name": "Bayelsa" + }, + { + "code": "BE", + "name": "Benue" + }, + { + "code": "BO", + "name": "Borno" + }, + { + "code": "CR", + "name": "Cross River" + }, + { + "code": "DE", + "name": "Delta" + }, + { + "code": "EB", + "name": "Ebonyi" + }, + { + "code": "ED", + "name": "Edo" + }, + { + "code": "EK", + "name": "Ekiti" + }, + { + "code": "EN", + "name": "Enugu" + }, + { + "code": "GO", + "name": "Gombe" + }, + { + "code": "IM", + "name": "Imo" + }, + { + "code": "JI", + "name": "Jigawa" + }, + { + "code": "KD", + "name": "Kaduna" + }, + { + "code": "KN", + "name": "Kano" + }, + { + "code": "KT", + "name": "Katsina" + }, + { + "code": "KE", + "name": "Kebbi" + }, + { + "code": "KO", + "name": "Kogi" + }, + { + "code": "KW", + "name": "Kwara" + }, + { + "code": "LA", + "name": "Lagos" + }, + { + "code": "NA", + "name": "Nasarawa" + }, + { + "code": "NI", + "name": "Niger" + }, + { + "code": "OG", + "name": "Ogun" + }, + { + "code": "ON", + "name": "Ondo" + }, + { + "code": "OS", + "name": "Osun" + }, + { + "code": "OY", + "name": "Oyo" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "RI", + "name": "Rivers" + }, + { + "code": "SO", + "name": "Sokoto" + }, + { + "code": "TA", + "name": "Taraba" + }, + { + "code": "YO", + "name": "Yobe" + }, + { + "code": "ZA", + "name": "Zamfara" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NU", + "name": "Niue", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NF", + "name": "Norfolk Island", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KP", + "name": "North Korea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MK", + "name": "North Macedonia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MP", + "name": "Northern Mariana Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NO", + "name": "Norway", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "OM", + "name": "Oman", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PK", + "name": "Pakistan", + "states": [{ + "code": "JK", + "name": "Azad Kashmir" + }, + { + "code": "BA", + "name": "Balochistan" + }, + { + "code": "TA", + "name": "FATA" + }, + { + "code": "GB", + "name": "Gilgit Baltistan" + }, + { + "code": "IS", + "name": "Islamabad Capital Territory" + }, + { + "code": "KP", + "name": "Khyber Pakhtunkhwa" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "SD", + "name": "Sindh" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PS", + "name": "Palestinian Territory", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PA", + "name": "Panama", + "states": [{ + "code": "PA-1", + "name": "Bocas del Toro" + }, + { + "code": "PA-2", + "name": "Coclé" + }, + { + "code": "PA-3", + "name": "Colón" + }, + { + "code": "PA-4", + "name": "Chiriquí" + }, + { + "code": "PA-5", + "name": "Darién" + }, + { + "code": "PA-6", + "name": "Herrera" + }, + { + "code": "PA-7", + "name": "Los Santos" + }, + { + "code": "PA-8", + "name": "Panamá" + }, + { + "code": "PA-9", + "name": "Veraguas" + }, + { + "code": "PA-10", + "name": "West Panamá" + }, + { + "code": "PA-EM", + "name": "Emberá" + }, + { + "code": "PA-KY", + "name": "Guna Yala" + }, + { + "code": "PA-NB", + "name": "Ngöbe-Buglé" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PG", + "name": "Papua New Guinea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PY", + "name": "Paraguay", + "states": [{ + "code": "PY-ASU", + "name": "Asunción" + }, + { + "code": "PY-1", + "name": "Concepción" + }, + { + "code": "PY-2", + "name": "San Pedro" + }, + { + "code": "PY-3", + "name": "Cordillera" + }, + { + "code": "PY-4", + "name": "Guairá" + }, + { + "code": "PY-5", + "name": "Caaguazú" + }, + { + "code": "PY-6", + "name": "Caazapá" + }, + { + "code": "PY-7", + "name": "Itapúa" + }, + { + "code": "PY-8", + "name": "Misiones" + }, + { + "code": "PY-9", + "name": "Paraguarí" + }, + { + "code": "PY-10", + "name": "Alto Paraná" + }, + { + "code": "PY-11", + "name": "Central" + }, + { + "code": "PY-12", + "name": "Ñeembucú" + }, + { + "code": "PY-13", + "name": "Amambay" + }, + { + "code": "PY-14", + "name": "Canindeyú" + }, + { + "code": "PY-15", + "name": "Presidente Hayes" + }, + { + "code": "PY-16", + "name": "Alto Paraguay" + }, + { + "code": "PY-17", + "name": "Boquerón" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PE", + "name": "Peru", + "states": [{ + "code": "CAL", + "name": "El Callao" + }, + { + "code": "LMA", + "name": "Municipalidad Metropolitana de Lima" + }, + { + "code": "AMA", + "name": "Amazonas" + }, + { + "code": "ANC", + "name": "Ancash" + }, + { + "code": "APU", + "name": "Apurímac" + }, + { + "code": "ARE", + "name": "Arequipa" + }, + { + "code": "AYA", + "name": "Ayacucho" + }, + { + "code": "CAJ", + "name": "Cajamarca" + }, + { + "code": "CUS", + "name": "Cusco" + }, + { + "code": "HUV", + "name": "Huancavelica" + }, + { + "code": "HUC", + "name": "Huánuco" + }, + { + "code": "ICA", + "name": "Ica" + }, + { + "code": "JUN", + "name": "Junín" + }, + { + "code": "LAL", + "name": "La Libertad" + }, + { + "code": "LAM", + "name": "Lambayeque" + }, + { + "code": "LIM", + "name": "Lima" + }, + { + "code": "LOR", + "name": "Loreto" + }, + { + "code": "MDD", + "name": "Madre de Dios" + }, + { + "code": "MOQ", + "name": "Moquegua" + }, + { + "code": "PAS", + "name": "Pasco" + }, + { + "code": "PIU", + "name": "Piura" + }, + { + "code": "PUN", + "name": "Puno" + }, + { + "code": "SAM", + "name": "San Martín" + }, + { + "code": "TAC", + "name": "Tacna" + }, + { + "code": "TUM", + "name": "Tumbes" + }, + { + "code": "UCA", + "name": "Ucayali" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PH", + "name": "Philippines", + "states": [{ + "code": "ABR", + "name": "Abra" + }, + { + "code": "AGN", + "name": "Agusan del Norte" + }, + { + "code": "AGS", + "name": "Agusan del Sur" + }, + { + "code": "AKL", + "name": "Aklan" + }, + { + "code": "ALB", + "name": "Albay" + }, + { + "code": "ANT", + "name": "Antique" + }, + { + "code": "APA", + "name": "Apayao" + }, + { + "code": "AUR", + "name": "Aurora" + }, + { + "code": "BAS", + "name": "Basilan" + }, + { + "code": "BAN", + "name": "Bataan" + }, + { + "code": "BTN", + "name": "Batanes" + }, + { + "code": "BTG", + "name": "Batangas" + }, + { + "code": "BEN", + "name": "Benguet" + }, + { + "code": "BIL", + "name": "Biliran" + }, + { + "code": "BOH", + "name": "Bohol" + }, + { + "code": "BUK", + "name": "Bukidnon" + }, + { + "code": "BUL", + "name": "Bulacan" + }, + { + "code": "CAG", + "name": "Cagayan" + }, + { + "code": "CAN", + "name": "Camarines Norte" + }, + { + "code": "CAS", + "name": "Camarines Sur" + }, + { + "code": "CAM", + "name": "Camiguin" + }, + { + "code": "CAP", + "name": "Capiz" + }, + { + "code": "CAT", + "name": "Catanduanes" + }, + { + "code": "CAV", + "name": "Cavite" + }, + { + "code": "CEB", + "name": "Cebu" + }, + { + "code": "COM", + "name": "Compostela Valley" + }, + { + "code": "NCO", + "name": "Cotabato" + }, + { + "code": "DAV", + "name": "Davao del Norte" + }, + { + "code": "DAS", + "name": "Davao del Sur" + }, + { + "code": "DAC", + "name": "Davao Occidental" + }, + { + "code": "DAO", + "name": "Davao Oriental" + }, + { + "code": "DIN", + "name": "Dinagat Islands" + }, + { + "code": "EAS", + "name": "Eastern Samar" + }, + { + "code": "GUI", + "name": "Guimaras" + }, + { + "code": "IFU", + "name": "Ifugao" + }, + { + "code": "ILN", + "name": "Ilocos Norte" + }, + { + "code": "ILS", + "name": "Ilocos Sur" + }, + { + "code": "ILI", + "name": "Iloilo" + }, + { + "code": "ISA", + "name": "Isabela" + }, + { + "code": "KAL", + "name": "Kalinga" + }, + { + "code": "LUN", + "name": "La Union" + }, + { + "code": "LAG", + "name": "Laguna" + }, + { + "code": "LAN", + "name": "Lanao del Norte" + }, + { + "code": "LAS", + "name": "Lanao del Sur" + }, + { + "code": "LEY", + "name": "Leyte" + }, + { + "code": "MAG", + "name": "Maguindanao" + }, + { + "code": "MAD", + "name": "Marinduque" + }, + { + "code": "MAS", + "name": "Masbate" + }, + { + "code": "MSC", + "name": "Misamis Occidental" + }, + { + "code": "MSR", + "name": "Misamis Oriental" + }, + { + "code": "MOU", + "name": "Mountain Province" + }, + { + "code": "NEC", + "name": "Negros Occidental" + }, + { + "code": "NER", + "name": "Negros Oriental" + }, + { + "code": "NSA", + "name": "Northern Samar" + }, + { + "code": "NUE", + "name": "Nueva Ecija" + }, + { + "code": "NUV", + "name": "Nueva Vizcaya" + }, + { + "code": "MDC", + "name": "Occidental Mindoro" + }, + { + "code": "MDR", + "name": "Oriental Mindoro" + }, + { + "code": "PLW", + "name": "Palawan" + }, + { + "code": "PAM", + "name": "Pampanga" + }, + { + "code": "PAN", + "name": "Pangasinan" + }, + { + "code": "QUE", + "name": "Quezon" + }, + { + "code": "QUI", + "name": "Quirino" + }, + { + "code": "RIZ", + "name": "Rizal" + }, + { + "code": "ROM", + "name": "Romblon" + }, + { + "code": "WSA", + "name": "Samar" + }, + { + "code": "SAR", + "name": "Sarangani" + }, + { + "code": "SIQ", + "name": "Siquijor" + }, + { + "code": "SOR", + "name": "Sorsogon" + }, + { + "code": "SCO", + "name": "South Cotabato" + }, + { + "code": "SLE", + "name": "Southern Leyte" + }, + { + "code": "SUK", + "name": "Sultan Kudarat" + }, + { + "code": "SLU", + "name": "Sulu" + }, + { + "code": "SUN", + "name": "Surigao del Norte" + }, + { + "code": "SUR", + "name": "Surigao del Sur" + }, + { + "code": "TAR", + "name": "Tarlac" + }, + { + "code": "TAW", + "name": "Tawi-Tawi" + }, + { + "code": "ZMB", + "name": "Zambales" + }, + { + "code": "ZAN", + "name": "Zamboanga del Norte" + }, + { + "code": "ZAS", + "name": "Zamboanga del Sur" + }, + { + "code": "ZSI", + "name": "Zamboanga Sibugay" + }, + { + "code": "00", + "name": "Metro Manila" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PN", + "name": "Pitcairn", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PL", + "name": "Poland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PT", + "name": "Portugal", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PR", + "name": "Puerto Rico", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "QA", + "name": "Qatar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RE", + "name": "Reunion", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RO", + "name": "Romania", + "states": [{ + "code": "AB", + "name": "Alba" + }, + { + "code": "AR", + "name": "Arad" + }, + { + "code": "AG", + "name": "Argeș" + }, + { + "code": "BC", + "name": "Bacău" + }, + { + "code": "BH", + "name": "Bihor" + }, + { + "code": "BN", + "name": "Bistrița-Năsăud" + }, + { + "code": "BT", + "name": "Botoșani" + }, + { + "code": "BR", + "name": "Brăila" + }, + { + "code": "BV", + "name": "Brașov" + }, + { + "code": "B", + "name": "București" + }, + { + "code": "BZ", + "name": "Buzău" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Caraș-Severin" + }, + { + "code": "CJ", + "name": "Cluj" + }, + { + "code": "CT", + "name": "Constanța" + }, + { + "code": "CV", + "name": "Covasna" + }, + { + "code": "DB", + "name": "Dâmbovița" + }, + { + "code": "DJ", + "name": "Dolj" + }, + { + "code": "GL", + "name": "Galați" + }, + { + "code": "GR", + "name": "Giurgiu" + }, + { + "code": "GJ", + "name": "Gorj" + }, + { + "code": "HR", + "name": "Harghita" + }, + { + "code": "HD", + "name": "Hunedoara" + }, + { + "code": "IL", + "name": "Ialomița" + }, + { + "code": "IS", + "name": "Iași" + }, + { + "code": "IF", + "name": "Ilfov" + }, + { + "code": "MM", + "name": "Maramureș" + }, + { + "code": "MH", + "name": "Mehedinți" + }, + { + "code": "MS", + "name": "Mureș" + }, + { + "code": "NT", + "name": "Neamț" + }, + { + "code": "OT", + "name": "Olt" + }, + { + "code": "PH", + "name": "Prahova" + }, + { + "code": "SJ", + "name": "Sălaj" + }, + { + "code": "SM", + "name": "Satu Mare" + }, + { + "code": "SB", + "name": "Sibiu" + }, + { + "code": "SV", + "name": "Suceava" + }, + { + "code": "TR", + "name": "Teleorman" + }, + { + "code": "TM", + "name": "Timiș" + }, + { + "code": "TL", + "name": "Tulcea" + }, + { + "code": "VL", + "name": "Vâlcea" + }, + { + "code": "VS", + "name": "Vaslui" + }, + { + "code": "VN", + "name": "Vrancea" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RU", + "name": "Russia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RW", + "name": "Rwanda", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ST", + "name": "São Tomé and Príncipe", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BL", + "name": "Saint Barthélemy", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SH", + "name": "Saint Helena", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KN", + "name": "Saint Kitts and Nevis", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LC", + "name": "Saint Lucia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SX", + "name": "Saint Martin (Dutch part)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MF", + "name": "Saint Martin (French part)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PM", + "name": "Saint Pierre and Miquelon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VC", + "name": "Saint Vincent and the Grenadines", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "WS", + "name": "Samoa", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SM", + "name": "San Marino", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SA", + "name": "Saudi Arabia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SN", + "name": "Senegal", + "states": [{ + "code": "SNDB", + "name": "Diourbel" + }, + { + "code": "SNDK", + "name": "Dakar" + }, + { + "code": "SNFK", + "name": "Fatick" + }, + { + "code": "SNKA", + "name": "Kaffrine" + }, + { + "code": "SNKD", + "name": "Kolda" + }, + { + "code": "SNKE", + "name": "Kédougou" + }, + { + "code": "SNKL", + "name": "Kaolack" + }, + { + "code": "SNLG", + "name": "Louga" + }, + { + "code": "SNMT", + "name": "Matam" + }, + { + "code": "SNSE", + "name": "Sédhiou" + }, + { + "code": "SNSL", + "name": "Saint-Louis" + }, + { + "code": "SNTC", + "name": "Tambacounda" + }, + { + "code": "SNTH", + "name": "Thiès" + }, + { + "code": "SNZG", + "name": "Ziguinchor" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RS", + "name": "Serbia", + "states": [{ + "code": "RS00", + "name": "Belgrade" + }, + { + "code": "RS14", + "name": "Bor" + }, + { + "code": "RS11", + "name": "Braničevo" + }, + { + "code": "RS02", + "name": "Central Banat" + }, + { + "code": "RS10", + "name": "Danube" + }, + { + "code": "RS23", + "name": "Jablanica" + }, + { + "code": "RS09", + "name": "Kolubara" + }, + { + "code": "RS08", + "name": "Mačva" + }, + { + "code": "RS17", + "name": "Morava" + }, + { + "code": "RS20", + "name": "Nišava" + }, + { + "code": "RS01", + "name": "North Bačka" + }, + { + "code": "RS03", + "name": "North Banat" + }, + { + "code": "RS24", + "name": "Pčinja" + }, + { + "code": "RS22", + "name": "Pirot" + }, + { + "code": "RS13", + "name": "Pomoravlje" + }, + { + "code": "RS19", + "name": "Rasina" + }, + { + "code": "RS18", + "name": "Raška" + }, + { + "code": "RS06", + "name": "South Bačka" + }, + { + "code": "RS04", + "name": "South Banat" + }, + { + "code": "RS07", + "name": "Srem" + }, + { + "code": "RS12", + "name": "Šumadija" + }, + { + "code": "RS21", + "name": "Toplica" + }, + { + "code": "RS05", + "name": "West Bačka" + }, + { + "code": "RS15", + "name": "Zaječar" + }, + { + "code": "RS16", + "name": "Zlatibor" + }, + { + "code": "RS25", + "name": "Kosovo" + }, + { + "code": "RS26", + "name": "Peć" + }, + { + "code": "RS27", + "name": "Prizren" + }, + { + "code": "RS28", + "name": "Kosovska Mitrovica" + }, + { + "code": "RS29", + "name": "Kosovo-Pomoravlje" + }, + { + "code": "RSKM", + "name": "Kosovo-Metohija" + }, + { + "code": "RSVO", + "name": "Vojvodina" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SC", + "name": "Seychelles", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SL", + "name": "Sierra Leone", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SG", + "name": "Singapore", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SK", + "name": "Slovakia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SI", + "name": "Slovenia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SB", + "name": "Solomon Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SO", + "name": "Somalia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZA", + "name": "South Africa", + "states": [{ + "code": "EC", + "name": "Eastern Cape" + }, + { + "code": "FS", + "name": "Free State" + }, + { + "code": "GP", + "name": "Gauteng" + }, + { + "code": "KZN", + "name": "KwaZulu-Natal" + }, + { + "code": "LP", + "name": "Limpopo" + }, + { + "code": "MP", + "name": "Mpumalanga" + }, + { + "code": "NC", + "name": "Northern Cape" + }, + { + "code": "NW", + "name": "North West" + }, + { + "code": "WC", + "name": "Western Cape" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GS", + "name": "South Georgia/Sandwich Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KR", + "name": "South Korea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SS", + "name": "South Sudan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ES", + "name": "Spain", + "states": [{ + "code": "C", + "name": "A Coruña" + }, + { + "code": "VI", + "name": "Araba/Álava" + }, + { + "code": "AB", + "name": "Albacete" + }, + { + "code": "A", + "name": "Alicante" + }, + { + "code": "AL", + "name": "Almería" + }, + { + "code": "O", + "name": "Asturias" + }, + { + "code": "AV", + "name": "Ávila" + }, + { + "code": "BA", + "name": "Badajoz" + }, + { + "code": "PM", + "name": "Baleares" + }, + { + "code": "B", + "name": "Barcelona" + }, + { + "code": "BU", + "name": "Burgos" + }, + { + "code": "CC", + "name": "Cáceres" + }, + { + "code": "CA", + "name": "Cádiz" + }, + { + "code": "S", + "name": "Cantabria" + }, + { + "code": "CS", + "name": "Castellón" + }, + { + "code": "CE", + "name": "Ceuta" + }, + { + "code": "CR", + "name": "Ciudad Real" + }, + { + "code": "CO", + "name": "Córdoba" + }, + { + "code": "CU", + "name": "Cuenca" + }, + { + "code": "GI", + "name": "Girona" + }, + { + "code": "GR", + "name": "Granada" + }, + { + "code": "GU", + "name": "Guadalajara" + }, + { + "code": "SS", + "name": "Gipuzkoa" + }, + { + "code": "H", + "name": "Huelva" + }, + { + "code": "HU", + "name": "Huesca" + }, + { + "code": "J", + "name": "Jaén" + }, + { + "code": "LO", + "name": "La Rioja" + }, + { + "code": "GC", + "name": "Las Palmas" + }, + { + "code": "LE", + "name": "León" + }, + { + "code": "L", + "name": "Lleida" + }, + { + "code": "LU", + "name": "Lugo" + }, + { + "code": "M", + "name": "Madrid" + }, + { + "code": "MA", + "name": "Málaga" + }, + { + "code": "ML", + "name": "Melilla" + }, + { + "code": "MU", + "name": "Murcia" + }, + { + "code": "NA", + "name": "Navarra" + }, + { + "code": "OR", + "name": "Ourense" + }, + { + "code": "P", + "name": "Palencia" + }, + { + "code": "PO", + "name": "Pontevedra" + }, + { + "code": "SA", + "name": "Salamanca" + }, + { + "code": "TF", + "name": "Santa Cruz de Tenerife" + }, + { + "code": "SG", + "name": "Segovia" + }, + { + "code": "SE", + "name": "Sevilla" + }, + { + "code": "SO", + "name": "Soria" + }, + { + "code": "T", + "name": "Tarragona" + }, + { + "code": "TE", + "name": "Teruel" + }, + { + "code": "TO", + "name": "Toledo" + }, + { + "code": "V", + "name": "Valencia" + }, + { + "code": "VA", + "name": "Valladolid" + }, + { + "code": "BI", + "name": "Biscay" + }, + { + "code": "ZA", + "name": "Zamora" + }, + { + "code": "Z", + "name": "Zaragoza" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LK", + "name": "Sri Lanka", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SD", + "name": "Sudan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SR", + "name": "Suriname", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SJ", + "name": "Svalbard and Jan Mayen", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SE", + "name": "Sweden", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CH", + "name": "Switzerland", + "states": [{ + "code": "AG", + "name": "Aargau" + }, + { + "code": "AR", + "name": "Appenzell Ausserrhoden" + }, + { + "code": "AI", + "name": "Appenzell Innerrhoden" + }, + { + "code": "BL", + "name": "Basel-Landschaft" + }, + { + "code": "BS", + "name": "Basel-Stadt" + }, + { + "code": "BE", + "name": "Bern" + }, + { + "code": "FR", + "name": "Fribourg" + }, + { + "code": "GE", + "name": "Geneva" + }, + { + "code": "GL", + "name": "Glarus" + }, + { + "code": "GR", + "name": "Graubünden" + }, + { + "code": "JU", + "name": "Jura" + }, + { + "code": "LU", + "name": "Luzern" + }, + { + "code": "NE", + "name": "Neuchâtel" + }, + { + "code": "NW", + "name": "Nidwalden" + }, + { + "code": "OW", + "name": "Obwalden" + }, + { + "code": "SH", + "name": "Schaffhausen" + }, + { + "code": "SZ", + "name": "Schwyz" + }, + { + "code": "SO", + "name": "Solothurn" + }, + { + "code": "SG", + "name": "St. Gallen" + }, + { + "code": "TG", + "name": "Thurgau" + }, + { + "code": "TI", + "name": "Ticino" + }, + { + "code": "UR", + "name": "Uri" + }, + { + "code": "VS", + "name": "Valais" + }, + { + "code": "VD", + "name": "Vaud" + }, + { + "code": "ZG", + "name": "Zug" + }, + { + "code": "ZH", + "name": "Zürich" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SY", + "name": "Syria", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TW", + "name": "Taiwan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TJ", + "name": "Tajikistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TZ", + "name": "Tanzania", + "states": [{ + "code": "TZ01", + "name": "Arusha" + }, + { + "code": "TZ02", + "name": "Dar es Salaam" + }, + { + "code": "TZ03", + "name": "Dodoma" + }, + { + "code": "TZ04", + "name": "Iringa" + }, + { + "code": "TZ05", + "name": "Kagera" + }, + { + "code": "TZ06", + "name": "Pemba North" + }, + { + "code": "TZ07", + "name": "Zanzibar North" + }, + { + "code": "TZ08", + "name": "Kigoma" + }, + { + "code": "TZ09", + "name": "Kilimanjaro" + }, + { + "code": "TZ10", + "name": "Pemba South" + }, + { + "code": "TZ11", + "name": "Zanzibar South" + }, + { + "code": "TZ12", + "name": "Lindi" + }, + { + "code": "TZ13", + "name": "Mara" + }, + { + "code": "TZ14", + "name": "Mbeya" + }, + { + "code": "TZ15", + "name": "Zanzibar West" + }, + { + "code": "TZ16", + "name": "Morogoro" + }, + { + "code": "TZ17", + "name": "Mtwara" + }, + { + "code": "TZ18", + "name": "Mwanza" + }, + { + "code": "TZ19", + "name": "Coast" + }, + { + "code": "TZ20", + "name": "Rukwa" + }, + { + "code": "TZ21", + "name": "Ruvuma" + }, + { + "code": "TZ22", + "name": "Shinyanga" + }, + { + "code": "TZ23", + "name": "Singida" + }, + { + "code": "TZ24", + "name": "Tabora" + }, + { + "code": "TZ25", + "name": "Tanga" + }, + { + "code": "TZ26", + "name": "Manyara" + }, + { + "code": "TZ27", + "name": "Geita" + }, + { + "code": "TZ28", + "name": "Katavi" + }, + { + "code": "TZ29", + "name": "Njombe" + }, + { + "code": "TZ30", + "name": "Simiyu" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TH", + "name": "Thailand", + "states": [{ + "code": "TH-37", + "name": "Amnat Charoen" + }, + { + "code": "TH-15", + "name": "Ang Thong" + }, + { + "code": "TH-14", + "name": "Ayutthaya" + }, + { + "code": "TH-10", + "name": "Bangkok" + }, + { + "code": "TH-38", + "name": "Bueng Kan" + }, + { + "code": "TH-31", + "name": "Buri Ram" + }, + { + "code": "TH-24", + "name": "Chachoengsao" + }, + { + "code": "TH-18", + "name": "Chai Nat" + }, + { + "code": "TH-36", + "name": "Chaiyaphum" + }, + { + "code": "TH-22", + "name": "Chanthaburi" + }, + { + "code": "TH-50", + "name": "Chiang Mai" + }, + { + "code": "TH-57", + "name": "Chiang Rai" + }, + { + "code": "TH-20", + "name": "Chonburi" + }, + { + "code": "TH-86", + "name": "Chumphon" + }, + { + "code": "TH-46", + "name": "Kalasin" + }, + { + "code": "TH-62", + "name": "Kamphaeng Phet" + }, + { + "code": "TH-71", + "name": "Kanchanaburi" + }, + { + "code": "TH-40", + "name": "Khon Kaen" + }, + { + "code": "TH-81", + "name": "Krabi" + }, + { + "code": "TH-52", + "name": "Lampang" + }, + { + "code": "TH-51", + "name": "Lamphun" + }, + { + "code": "TH-42", + "name": "Loei" + }, + { + "code": "TH-16", + "name": "Lopburi" + }, + { + "code": "TH-58", + "name": "Mae Hong Son" + }, + { + "code": "TH-44", + "name": "Maha Sarakham" + }, + { + "code": "TH-49", + "name": "Mukdahan" + }, + { + "code": "TH-26", + "name": "Nakhon Nayok" + }, + { + "code": "TH-73", + "name": "Nakhon Pathom" + }, + { + "code": "TH-48", + "name": "Nakhon Phanom" + }, + { + "code": "TH-30", + "name": "Nakhon Ratchasima" + }, + { + "code": "TH-60", + "name": "Nakhon Sawan" + }, + { + "code": "TH-80", + "name": "Nakhon Si Thammarat" + }, + { + "code": "TH-55", + "name": "Nan" + }, + { + "code": "TH-96", + "name": "Narathiwat" + }, + { + "code": "TH-39", + "name": "Nong Bua Lam Phu" + }, + { + "code": "TH-43", + "name": "Nong Khai" + }, + { + "code": "TH-12", + "name": "Nonthaburi" + }, + { + "code": "TH-13", + "name": "Pathum Thani" + }, + { + "code": "TH-94", + "name": "Pattani" + }, + { + "code": "TH-82", + "name": "Phang Nga" + }, + { + "code": "TH-93", + "name": "Phatthalung" + }, + { + "code": "TH-56", + "name": "Phayao" + }, + { + "code": "TH-67", + "name": "Phetchabun" + }, + { + "code": "TH-76", + "name": "Phetchaburi" + }, + { + "code": "TH-66", + "name": "Phichit" + }, + { + "code": "TH-65", + "name": "Phitsanulok" + }, + { + "code": "TH-54", + "name": "Phrae" + }, + { + "code": "TH-83", + "name": "Phuket" + }, + { + "code": "TH-25", + "name": "Prachin Buri" + }, + { + "code": "TH-77", + "name": "Prachuap Khiri Khan" + }, + { + "code": "TH-85", + "name": "Ranong" + }, + { + "code": "TH-70", + "name": "Ratchaburi" + }, + { + "code": "TH-21", + "name": "Rayong" + }, + { + "code": "TH-45", + "name": "Roi Et" + }, + { + "code": "TH-27", + "name": "Sa Kaeo" + }, + { + "code": "TH-47", + "name": "Sakon Nakhon" + }, + { + "code": "TH-11", + "name": "Samut Prakan" + }, + { + "code": "TH-74", + "name": "Samut Sakhon" + }, + { + "code": "TH-75", + "name": "Samut Songkhram" + }, + { + "code": "TH-19", + "name": "Saraburi" + }, + { + "code": "TH-91", + "name": "Satun" + }, + { + "code": "TH-17", + "name": "Sing Buri" + }, + { + "code": "TH-33", + "name": "Sisaket" + }, + { + "code": "TH-90", + "name": "Songkhla" + }, + { + "code": "TH-64", + "name": "Sukhothai" + }, + { + "code": "TH-72", + "name": "Suphan Buri" + }, + { + "code": "TH-84", + "name": "Surat Thani" + }, + { + "code": "TH-32", + "name": "Surin" + }, + { + "code": "TH-63", + "name": "Tak" + }, + { + "code": "TH-92", + "name": "Trang" + }, + { + "code": "TH-23", + "name": "Trat" + }, + { + "code": "TH-34", + "name": "Ubon Ratchathani" + }, + { + "code": "TH-41", + "name": "Udon Thani" + }, + { + "code": "TH-61", + "name": "Uthai Thani" + }, + { + "code": "TH-53", + "name": "Uttaradit" + }, + { + "code": "TH-95", + "name": "Yala" + }, + { + "code": "TH-35", + "name": "Yasothon" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TL", + "name": "Timor-Leste", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TG", + "name": "Togo", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TK", + "name": "Tokelau", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TO", + "name": "Tonga", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TT", + "name": "Trinidad and Tobago", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TN", + "name": "Tunisia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TR", + "name": "Turkey", + "states": [{ + "code": "TR01", + "name": "Adana" + }, + { + "code": "TR02", + "name": "Adıyaman" + }, + { + "code": "TR03", + "name": "Afyon" + }, + { + "code": "TR04", + "name": "Ağrı" + }, + { + "code": "TR05", + "name": "Amasya" + }, + { + "code": "TR06", + "name": "Ankara" + }, + { + "code": "TR07", + "name": "Antalya" + }, + { + "code": "TR08", + "name": "Artvin" + }, + { + "code": "TR09", + "name": "Aydın" + }, + { + "code": "TR10", + "name": "Balıkesir" + }, + { + "code": "TR11", + "name": "Bilecik" + }, + { + "code": "TR12", + "name": "Bingöl" + }, + { + "code": "TR13", + "name": "Bitlis" + }, + { + "code": "TR14", + "name": "Bolu" + }, + { + "code": "TR15", + "name": "Burdur" + }, + { + "code": "TR16", + "name": "Bursa" + }, + { + "code": "TR17", + "name": "Çanakkale" + }, + { + "code": "TR18", + "name": "Çankırı" + }, + { + "code": "TR19", + "name": "Çorum" + }, + { + "code": "TR20", + "name": "Denizli" + }, + { + "code": "TR21", + "name": "Diyarbakır" + }, + { + "code": "TR22", + "name": "Edirne" + }, + { + "code": "TR23", + "name": "Elazığ" + }, + { + "code": "TR24", + "name": "Erzincan" + }, + { + "code": "TR25", + "name": "Erzurum" + }, + { + "code": "TR26", + "name": "Eskişehir" + }, + { + "code": "TR27", + "name": "Gaziantep" + }, + { + "code": "TR28", + "name": "Giresun" + }, + { + "code": "TR29", + "name": "Gümüşhane" + }, + { + "code": "TR30", + "name": "Hakkari" + }, + { + "code": "TR31", + "name": "Hatay" + }, + { + "code": "TR32", + "name": "Isparta" + }, + { + "code": "TR33", + "name": "İçel" + }, + { + "code": "TR34", + "name": "İstanbul" + }, + { + "code": "TR35", + "name": "İzmir" + }, + { + "code": "TR36", + "name": "Kars" + }, + { + "code": "TR37", + "name": "Kastamonu" + }, + { + "code": "TR38", + "name": "Kayseri" + }, + { + "code": "TR39", + "name": "Kırklareli" + }, + { + "code": "TR40", + "name": "Kırşehir" + }, + { + "code": "TR41", + "name": "Kocaeli" + }, + { + "code": "TR42", + "name": "Konya" + }, + { + "code": "TR43", + "name": "Kütahya" + }, + { + "code": "TR44", + "name": "Malatya" + }, + { + "code": "TR45", + "name": "Manisa" + }, + { + "code": "TR46", + "name": "Kahramanmaraş" + }, + { + "code": "TR47", + "name": "Mardin" + }, + { + "code": "TR48", + "name": "Muğla" + }, + { + "code": "TR49", + "name": "Muş" + }, + { + "code": "TR50", + "name": "Nevşehir" + }, + { + "code": "TR51", + "name": "Niğde" + }, + { + "code": "TR52", + "name": "Ordu" + }, + { + "code": "TR53", + "name": "Rize" + }, + { + "code": "TR54", + "name": "Sakarya" + }, + { + "code": "TR55", + "name": "Samsun" + }, + { + "code": "TR56", + "name": "Siirt" + }, + { + "code": "TR57", + "name": "Sinop" + }, + { + "code": "TR58", + "name": "Sivas" + }, + { + "code": "TR59", + "name": "Tekirdağ" + }, + { + "code": "TR60", + "name": "Tokat" + }, + { + "code": "TR61", + "name": "Trabzon" + }, + { + "code": "TR62", + "name": "Tunceli" + }, + { + "code": "TR63", + "name": "Şanlıurfa" + }, + { + "code": "TR64", + "name": "Uşak" + }, + { + "code": "TR65", + "name": "Van" + }, + { + "code": "TR66", + "name": "Yozgat" + }, + { + "code": "TR67", + "name": "Zonguldak" + }, + { + "code": "TR68", + "name": "Aksaray" + }, + { + "code": "TR69", + "name": "Bayburt" + }, + { + "code": "TR70", + "name": "Karaman" + }, + { + "code": "TR71", + "name": "Kırıkkale" + }, + { + "code": "TR72", + "name": "Batman" + }, + { + "code": "TR73", + "name": "Şırnak" + }, + { + "code": "TR74", + "name": "Bartın" + }, + { + "code": "TR75", + "name": "Ardahan" + }, + { + "code": "TR76", + "name": "Iğdır" + }, + { + "code": "TR77", + "name": "Yalova" + }, + { + "code": "TR78", + "name": "Karabük" + }, + { + "code": "TR79", + "name": "Kilis" + }, + { + "code": "TR80", + "name": "Osmaniye" + }, + { + "code": "TR81", + "name": "Düzce" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TM", + "name": "Turkmenistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TC", + "name": "Turks and Caicos Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TV", + "name": "Tuvalu", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UG", + "name": "Uganda", + "states": [{ + "code": "UG314", + "name": "Abim" + }, + { + "code": "UG301", + "name": "Adjumani" + }, + { + "code": "UG322", + "name": "Agago" + }, + { + "code": "UG323", + "name": "Alebtong" + }, + { + "code": "UG315", + "name": "Amolatar" + }, + { + "code": "UG324", + "name": "Amudat" + }, + { + "code": "UG216", + "name": "Amuria" + }, + { + "code": "UG316", + "name": "Amuru" + }, + { + "code": "UG302", + "name": "Apac" + }, + { + "code": "UG303", + "name": "Arua" + }, + { + "code": "UG217", + "name": "Budaka" + }, + { + "code": "UG218", + "name": "Bududa" + }, + { + "code": "UG201", + "name": "Bugiri" + }, + { + "code": "UG235", + "name": "Bugweri" + }, + { + "code": "UG420", + "name": "Buhweju" + }, + { + "code": "UG117", + "name": "Buikwe" + }, + { + "code": "UG219", + "name": "Bukedea" + }, + { + "code": "UG118", + "name": "Bukomansimbi" + }, + { + "code": "UG220", + "name": "Bukwa" + }, + { + "code": "UG225", + "name": "Bulambuli" + }, + { + "code": "UG416", + "name": "Buliisa" + }, + { + "code": "UG401", + "name": "Bundibugyo" + }, + { + "code": "UG430", + "name": "Bunyangabu" + }, + { + "code": "UG402", + "name": "Bushenyi" + }, + { + "code": "UG202", + "name": "Busia" + }, + { + "code": "UG221", + "name": "Butaleja" + }, + { + "code": "UG119", + "name": "Butambala" + }, + { + "code": "UG233", + "name": "Butebo" + }, + { + "code": "UG120", + "name": "Buvuma" + }, + { + "code": "UG226", + "name": "Buyende" + }, + { + "code": "UG317", + "name": "Dokolo" + }, + { + "code": "UG121", + "name": "Gomba" + }, + { + "code": "UG304", + "name": "Gulu" + }, + { + "code": "UG403", + "name": "Hoima" + }, + { + "code": "UG417", + "name": "Ibanda" + }, + { + "code": "UG203", + "name": "Iganga" + }, + { + "code": "UG418", + "name": "Isingiro" + }, + { + "code": "UG204", + "name": "Jinja" + }, + { + "code": "UG318", + "name": "Kaabong" + }, + { + "code": "UG404", + "name": "Kabale" + }, + { + "code": "UG405", + "name": "Kabarole" + }, + { + "code": "UG213", + "name": "Kaberamaido" + }, + { + "code": "UG427", + "name": "Kagadi" + }, + { + "code": "UG428", + "name": "Kakumiro" + }, + { + "code": "UG101", + "name": "Kalangala" + }, + { + "code": "UG222", + "name": "Kaliro" + }, + { + "code": "UG122", + "name": "Kalungu" + }, + { + "code": "UG102", + "name": "Kampala" + }, + { + "code": "UG205", + "name": "Kamuli" + }, + { + "code": "UG413", + "name": "Kamwenge" + }, + { + "code": "UG414", + "name": "Kanungu" + }, + { + "code": "UG206", + "name": "Kapchorwa" + }, + { + "code": "UG236", + "name": "Kapelebyong" + }, + { + "code": "UG126", + "name": "Kasanda" + }, + { + "code": "UG406", + "name": "Kasese" + }, + { + "code": "UG207", + "name": "Katakwi" + }, + { + "code": "UG112", + "name": "Kayunga" + }, + { + "code": "UG407", + "name": "Kibaale" + }, + { + "code": "UG103", + "name": "Kiboga" + }, + { + "code": "UG227", + "name": "Kibuku" + }, + { + "code": "UG432", + "name": "Kikuube" + }, + { + "code": "UG419", + "name": "Kiruhura" + }, + { + "code": "UG421", + "name": "Kiryandongo" + }, + { + "code": "UG408", + "name": "Kisoro" + }, + { + "code": "UG305", + "name": "Kitgum" + }, + { + "code": "UG319", + "name": "Koboko" + }, + { + "code": "UG325", + "name": "Kole" + }, + { + "code": "UG306", + "name": "Kotido" + }, + { + "code": "UG208", + "name": "Kumi" + }, + { + "code": "UG333", + "name": "Kwania" + }, + { + "code": "UG228", + "name": "Kween" + }, + { + "code": "UG123", + "name": "Kyankwanzi" + }, + { + "code": "UG422", + "name": "Kyegegwa" + }, + { + "code": "UG415", + "name": "Kyenjojo" + }, + { + "code": "UG125", + "name": "Kyotera" + }, + { + "code": "UG326", + "name": "Lamwo" + }, + { + "code": "UG307", + "name": "Lira" + }, + { + "code": "UG229", + "name": "Luuka" + }, + { + "code": "UG104", + "name": "Luwero" + }, + { + "code": "UG124", + "name": "Lwengo" + }, + { + "code": "UG114", + "name": "Lyantonde" + }, + { + "code": "UG223", + "name": "Manafwa" + }, + { + "code": "UG320", + "name": "Maracha" + }, + { + "code": "UG105", + "name": "Masaka" + }, + { + "code": "UG409", + "name": "Masindi" + }, + { + "code": "UG214", + "name": "Mayuge" + }, + { + "code": "UG209", + "name": "Mbale" + }, + { + "code": "UG410", + "name": "Mbarara" + }, + { + "code": "UG423", + "name": "Mitooma" + }, + { + "code": "UG115", + "name": "Mityana" + }, + { + "code": "UG308", + "name": "Moroto" + }, + { + "code": "UG309", + "name": "Moyo" + }, + { + "code": "UG106", + "name": "Mpigi" + }, + { + "code": "UG107", + "name": "Mubende" + }, + { + "code": "UG108", + "name": "Mukono" + }, + { + "code": "UG334", + "name": "Nabilatuk" + }, + { + "code": "UG311", + "name": "Nakapiripirit" + }, + { + "code": "UG116", + "name": "Nakaseke" + }, + { + "code": "UG109", + "name": "Nakasongola" + }, + { + "code": "UG230", + "name": "Namayingo" + }, + { + "code": "UG234", + "name": "Namisindwa" + }, + { + "code": "UG224", + "name": "Namutumba" + }, + { + "code": "UG327", + "name": "Napak" + }, + { + "code": "UG310", + "name": "Nebbi" + }, + { + "code": "UG231", + "name": "Ngora" + }, + { + "code": "UG424", + "name": "Ntoroko" + }, + { + "code": "UG411", + "name": "Ntungamo" + }, + { + "code": "UG328", + "name": "Nwoya" + }, + { + "code": "UG331", + "name": "Omoro" + }, + { + "code": "UG329", + "name": "Otuke" + }, + { + "code": "UG321", + "name": "Oyam" + }, + { + "code": "UG312", + "name": "Pader" + }, + { + "code": "UG332", + "name": "Pakwach" + }, + { + "code": "UG210", + "name": "Pallisa" + }, + { + "code": "UG110", + "name": "Rakai" + }, + { + "code": "UG429", + "name": "Rubanda" + }, + { + "code": "UG425", + "name": "Rubirizi" + }, + { + "code": "UG431", + "name": "Rukiga" + }, + { + "code": "UG412", + "name": "Rukungiri" + }, + { + "code": "UG111", + "name": "Sembabule" + }, + { + "code": "UG232", + "name": "Serere" + }, + { + "code": "UG426", + "name": "Sheema" + }, + { + "code": "UG215", + "name": "Sironko" + }, + { + "code": "UG211", + "name": "Soroti" + }, + { + "code": "UG212", + "name": "Tororo" + }, + { + "code": "UG113", + "name": "Wakiso" + }, + { + "code": "UG313", + "name": "Yumbe" + }, + { + "code": "UG330", + "name": "Zombo" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UA", + "name": "Ukraine", + "states": [{ + "code": "VN", + "name": "Vinnytsia Oblast" + }, + { + "code": "VL", + "name": "Volyn Oblast" + }, + { + "code": "DP", + "name": "Dnipropetrovsk Oblast" + }, + { + "code": "DT", + "name": "Donetsk Oblast" + }, + { + "code": "ZT", + "name": "Zhytomyr Oblast" + }, + { + "code": "ZK", + "name": "Zakarpattia Oblast" + }, + { + "code": "ZP", + "name": "Zaporizhzhia Oblast" + }, + { + "code": "IF", + "name": "Ivano-Frankivsk Oblast" + }, + { + "code": "KV", + "name": "Kyiv Oblast" + }, + { + "code": "KH", + "name": "Kirovohrad Oblast" + }, + { + "code": "LH", + "name": "Luhansk Oblast" + }, + { + "code": "LV", + "name": "Lviv Oblast" + }, + { + "code": "MY", + "name": "Mykolaiv Oblast" + }, + { + "code": "OD", + "name": "Odessa Oblast" + }, + { + "code": "PL", + "name": "Poltava Oblast" + }, + { + "code": "RV", + "name": "Rivne Oblast" + }, + { + "code": "SM", + "name": "Sumy Oblast" + }, + { + "code": "TP", + "name": "Ternopil Oblast" + }, + { + "code": "KK", + "name": "Kharkiv Oblast" + }, + { + "code": "KS", + "name": "Kherson Oblast" + }, + { + "code": "KM", + "name": "Khmelnytskyi Oblast" + }, + { + "code": "CK", + "name": "Cherkasy Oblast" + }, + { + "code": "CH", + "name": "Chernihiv Oblast" + }, + { + "code": "CV", + "name": "Chernivtsi Oblast" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AE", + "name": "United Arab Emirates", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GB", + "name": "United Kingdom (UK)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "US", + "name": "United States (US)", + "states": [{ + "code": "AL", + "name": "Alabama" + }, + { + "code": "AK", + "name": "Alaska" + }, + { + "code": "AZ", + "name": "Arizona" + }, + { + "code": "AR", + "name": "Arkansas" + }, + { + "code": "CA", + "name": "California" + }, + { + "code": "CO", + "name": "Colorado" + }, + { + "code": "CT", + "name": "Connecticut" + }, + { + "code": "DE", + "name": "Delaware" + }, + { + "code": "DC", + "name": "District Of Columbia" + }, + { + "code": "FL", + "name": "Florida" + }, + { + "code": "GA", + "name": "Georgia" + }, + { + "code": "HI", + "name": "Hawaii" + }, + { + "code": "ID", + "name": "Idaho" + }, + { + "code": "IL", + "name": "Illinois" + }, + { + "code": "IN", + "name": "Indiana" + }, + { + "code": "IA", + "name": "Iowa" + }, + { + "code": "KS", + "name": "Kansas" + }, + { + "code": "KY", + "name": "Kentucky" + }, + { + "code": "LA", + "name": "Louisiana" + }, + { + "code": "ME", + "name": "Maine" + }, + { + "code": "MD", + "name": "Maryland" + }, + { + "code": "MA", + "name": "Massachusetts" + }, + { + "code": "MI", + "name": "Michigan" + }, + { + "code": "MN", + "name": "Minnesota" + }, + { + "code": "MS", + "name": "Mississippi" + }, + { + "code": "MO", + "name": "Missouri" + }, + { + "code": "MT", + "name": "Montana" + }, + { + "code": "NE", + "name": "Nebraska" + }, + { + "code": "NV", + "name": "Nevada" + }, + { + "code": "NH", + "name": "New Hampshire" + }, + { + "code": "NJ", + "name": "New Jersey" + }, + { + "code": "NM", + "name": "New Mexico" + }, + { + "code": "NY", + "name": "New York" + }, + { + "code": "NC", + "name": "North Carolina" + }, + { + "code": "ND", + "name": "North Dakota" + }, + { + "code": "OH", + "name": "Ohio" + }, + { + "code": "OK", + "name": "Oklahoma" + }, + { + "code": "OR", + "name": "Oregon" + }, + { + "code": "PA", + "name": "Pennsylvania" + }, + { + "code": "RI", + "name": "Rhode Island" + }, + { + "code": "SC", + "name": "South Carolina" + }, + { + "code": "SD", + "name": "South Dakota" + }, + { + "code": "TN", + "name": "Tennessee" + }, + { + "code": "TX", + "name": "Texas" + }, + { + "code": "UT", + "name": "Utah" + }, + { + "code": "VT", + "name": "Vermont" + }, + { + "code": "VA", + "name": "Virginia" + }, + { + "code": "WA", + "name": "Washington" + }, + { + "code": "WV", + "name": "West Virginia" + }, + { + "code": "WI", + "name": "Wisconsin" + }, + { + "code": "WY", + "name": "Wyoming" + }, + { + "code": "AA", + "name": "Armed Forces (AA)" + }, + { + "code": "AE", + "name": "Armed Forces (AE)" + }, + { + "code": "AP", + "name": "Armed Forces (AP)" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UM", + "name": "United States (US) Minor Outlying Islands", + "states": [{ + "code": 81, + "name": "Baker Island" + }, + { + "code": 84, + "name": "Howland Island" + }, + { + "code": 86, + "name": "Jarvis Island" + }, + { + "code": 67, + "name": "Johnston Atoll" + }, + { + "code": 89, + "name": "Kingman Reef" + }, + { + "code": 71, + "name": "Midway Atoll" + }, + { + "code": 76, + "name": "Navassa Island" + }, + { + "code": 95, + "name": "Palmyra Atoll" + }, + { + "code": 79, + "name": "Wake Island" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UY", + "name": "Uruguay", + "states": [{ + "code": "UY-AR", + "name": "Artigas" + }, + { + "code": "UY-CA", + "name": "Canelones" + }, + { + "code": "UY-CL", + "name": "Cerro Largo" + }, + { + "code": "UY-CO", + "name": "Colonia" + }, + { + "code": "UY-DU", + "name": "Durazno" + }, + { + "code": "UY-FS", + "name": "Flores" + }, + { + "code": "UY-FD", + "name": "Florida" + }, + { + "code": "UY-LA", + "name": "Lavalleja" + }, + { + "code": "UY-MA", + "name": "Maldonado" + }, + { + "code": "UY-MO", + "name": "Montevideo" + }, + { + "code": "UY-PA", + "name": "Paysandú" + }, + { + "code": "UY-RN", + "name": "Río Negro" + }, + { + "code": "UY-RV", + "name": "Rivera" + }, + { + "code": "UY-RO", + "name": "Rocha" + }, + { + "code": "UY-SA", + "name": "Salto" + }, + { + "code": "UY-SJ", + "name": "San José" + }, + { + "code": "UY-SO", + "name": "Soriano" + }, + { + "code": "UY-TA", + "name": "Tacuarembó" + }, + { + "code": "UY-TT", + "name": "Treinta y Tres" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UZ", + "name": "Uzbekistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VU", + "name": "Vanuatu", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VA", + "name": "Vatican", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VE", + "name": "Venezuela", + "states": [{ + "code": "VE-A", + "name": "Capital" + }, + { + "code": "VE-B", + "name": "Anzoátegui" + }, + { + "code": "VE-C", + "name": "Apure" + }, + { + "code": "VE-D", + "name": "Aragua" + }, + { + "code": "VE-E", + "name": "Barinas" + }, + { + "code": "VE-F", + "name": "Bolívar" + }, + { + "code": "VE-G", + "name": "Carabobo" + }, + { + "code": "VE-H", + "name": "Cojedes" + }, + { + "code": "VE-I", + "name": "Falcón" + }, + { + "code": "VE-J", + "name": "Guárico" + }, + { + "code": "VE-K", + "name": "Lara" + }, + { + "code": "VE-L", + "name": "Mérida" + }, + { + "code": "VE-M", + "name": "Miranda" + }, + { + "code": "VE-N", + "name": "Monagas" + }, + { + "code": "VE-O", + "name": "Nueva Esparta" + }, + { + "code": "VE-P", + "name": "Portuguesa" + }, + { + "code": "VE-R", + "name": "Sucre" + }, + { + "code": "VE-S", + "name": "Táchira" + }, + { + "code": "VE-T", + "name": "Trujillo" + }, + { + "code": "VE-U", + "name": "Yaracuy" + }, + { + "code": "VE-V", + "name": "Zulia" + }, + { + "code": "VE-W", + "name": "Federal Dependencies" + }, + { + "code": "VE-X", + "name": "La Guaira (Vargas)" + }, + { + "code": "VE-Y", + "name": "Delta Amacuro" + }, + { + "code": "VE-Z", + "name": "Amazonas" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VN", + "name": "Vietnam", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VG", + "name": "Virgin Islands (British)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VI", + "name": "Virgin Islands (US)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "WF", + "name": "Wallis and Futuna", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EH", + "name": "Western Sahara", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "YE", + "name": "Yemen", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZM", + "name": "Zambia", + "states": [{ + "code": "ZM-01", + "name": "Western" + }, + { + "code": "ZM-02", + "name": "Central" + }, + { + "code": "ZM-03", + "name": "Eastern" + }, + { + "code": "ZM-04", + "name": "Luapula" + }, + { + "code": "ZM-05", + "name": "Northern" + }, + { + "code": "ZM-06", + "name": "North-Western" + }, + { + "code": "ZM-07", + "name": "Southern" + }, + { + "code": "ZM-08", + "name": "Copperbelt" + }, + { + "code": "ZM-09", + "name": "Lusaka" + }, + { + "code": "ZM-10", + "name": "Muchinga" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZW", + "name": "Zimbabwe", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + }); + + test('can view country data', async ({ + request + }) => { + // call API to retrieve a specific country data + const response = await request.get('/wp-json/wc/v3/data/countries/au'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "AU", + "name": "Australia", + "states": [{ + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "code": "NSW", + "name": "New South Wales" + }, + { + "code": "NT", + "name": "Northern Territory" + }, + { + "code": "QLD", + "name": "Queensland" + }, + { + "code": "SA", + "name": "South Australia" + }, + { + "code": "TAS", + "name": "Tasmania" + }, + { + "code": "VIC", + "name": "Victoria" + }, + { + "code": "WA", + "name": "Western Australia" + } + ], + }) + ); + }); + + test('can view all currencies', async ({ + request + }) => { + // call API to retrieve all currencies + const response = await request.get('/wp-json/wc/v3/data/currencies'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AED", + "name": "United Arab Emirates dirham", + "symbol": "د.إ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AED") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AFN", + "name": "Afghan afghani", + "symbol": "؋", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AFN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ALL", + "name": "Albanian lek", + "symbol": "L", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ALL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AMD", + "name": "Armenian dram", + "symbol": "AMD", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ANG", + "name": "Netherlands Antillean guilder", + "symbol": "ƒ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ANG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AOA", + "name": "Angolan kwanza", + "symbol": "Kz", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AOA") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ARS", + "name": "Argentine peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ARS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AUD", + "name": "Australian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AUD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AWG", + "name": "Aruban florin", + "symbol": "Afl.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AWG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AZN", + "name": "Azerbaijani manat", + "symbol": "AZN", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AZN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BAM", + "name": "Bosnia and Herzegovina convertible mark", + "symbol": "KM", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BAM") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BBD", + "name": "Barbadian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BBD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BDT", + "name": "Bangladeshi taka", + "symbol": "৳ ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BDT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BGN", + "name": "Bulgarian lev", + "symbol": "лв.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BGN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BHD", + "name": "Bahraini dinar", + "symbol": ".د.ب", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BHD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BIF", + "name": "Burundian franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BIF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BMD", + "name": "Bermudian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BND", + "name": "Brunei dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BND") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BOB", + "name": "Bolivian boliviano", + "symbol": "Bs.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BOB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BRL", + "name": "Brazilian real", + "symbol": "R$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BRL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BSD", + "name": "Bahamian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BSD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BTC", + "name": "Bitcoin", + "symbol": "฿", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BTC") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BTN", + "name": "Bhutanese ngultrum", + "symbol": "Nu.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BTN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BWP", + "name": "Botswana pula", + "symbol": "P", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BWP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BYR", + "name": "Belarusian ruble (old)", + "symbol": "Br", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BYR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BYN", + "name": "Belarusian ruble", + "symbol": "Br", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BYN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BZD", + "name": "Belize dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BZD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CAD", + "name": "Canadian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CAD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CDF", + "name": "Congolese franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CDF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CHF", + "name": "Swiss franc", + "symbol": "CHF", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CHF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CLP", + "name": "Chilean peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CLP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CNY", + "name": "Chinese yuan", + "symbol": "¥", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CNY") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "COP", + "name": "Colombian peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/COP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CRC", + "name": "Costa Rican colón", + "symbol": "₡", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CRC") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CUC", + "name": "Cuban convertible peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CUC") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CUP", + "name": "Cuban peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CUP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CVE", + "name": "Cape Verdean escudo", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CVE") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CZK", + "name": "Czech koruna", + "symbol": "Kč", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CZK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DJF", + "name": "Djiboutian franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DJF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DKK", + "name": "Danish krone", + "symbol": "kr.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DKK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DOP", + "name": "Dominican peso", + "symbol": "RD$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DOP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DZD", + "name": "Algerian dinar", + "symbol": "د.ج", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DZD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EGP", + "name": "Egyptian pound", + "symbol": "EGP", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/EGP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ERN", + "name": "Eritrean nakfa", + "symbol": "Nfk", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ERN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ETB", + "name": "Ethiopian birr", + "symbol": "Br", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ETB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EUR", + "name": "Euro", + "symbol": "€", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/EUR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FJD", + "name": "Fijian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/FJD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FKP", + "name": "Falkland Islands pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/FKP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GBP", + "name": "Pound sterling", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GBP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GEL", + "name": "Georgian lari", + "symbol": "₾", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GEL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GGP", + "name": "Guernsey pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GGP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GHS", + "name": "Ghana cedi", + "symbol": "₵", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GHS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GIP", + "name": "Gibraltar pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GIP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GMD", + "name": "Gambian dalasi", + "symbol": "D", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GNF", + "name": "Guinean franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GNF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GTQ", + "name": "Guatemalan quetzal", + "symbol": "Q", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GTQ") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GYD", + "name": "Guyanese dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GYD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HKD", + "name": "Hong Kong dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HKD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HNL", + "name": "Honduran lempira", + "symbol": "L", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HNL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HRK", + "name": "Croatian kuna", + "symbol": "kn", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HRK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HTG", + "name": "Haitian gourde", + "symbol": "G", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HTG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HUF", + "name": "Hungarian forint", + "symbol": "Ft", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HUF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IDR", + "name": "Indonesian rupiah", + "symbol": "Rp", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IDR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ILS", + "name": "Israeli new shekel", + "symbol": "₪", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ILS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IMP", + "name": "Manx pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IMP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "INR", + "name": "Indian rupee", + "symbol": "₹", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/INR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IQD", + "name": "Iraqi dinar", + "symbol": "د.ع", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IQD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IRR", + "name": "Iranian rial", + "symbol": "﷼", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IRR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IRT", + "name": "Iranian toman", + "symbol": "تومان", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IRT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ISK", + "name": "Icelandic króna", + "symbol": "kr.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ISK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JEP", + "name": "Jersey pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JEP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JMD", + "name": "Jamaican dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JOD", + "name": "Jordanian dinar", + "symbol": "د.ا", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JOD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JPY", + "name": "Japanese yen", + "symbol": "¥", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JPY") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KES", + "name": "Kenyan shilling", + "symbol": "KSh", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KES") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KGS", + "name": "Kyrgyzstani som", + "symbol": "сом", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KGS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KHR", + "name": "Cambodian riel", + "symbol": "៛", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KHR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KMF", + "name": "Comorian franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KMF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KPW", + "name": "North Korean won", + "symbol": "₩", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KPW") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KRW", + "name": "South Korean won", + "symbol": "₩", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KRW") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KWD", + "name": "Kuwaiti dinar", + "symbol": "د.ك", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KWD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KYD", + "name": "Cayman Islands dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KYD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KZT", + "name": "Kazakhstani tenge", + "symbol": "₸", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KZT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LAK", + "name": "Lao kip", + "symbol": "₭", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LAK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LBP", + "name": "Lebanese pound", + "symbol": "ل.ل", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LBP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LKR", + "name": "Sri Lankan rupee", + "symbol": "රු", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LKR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LRD", + "name": "Liberian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LRD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LSL", + "name": "Lesotho loti", + "symbol": "L", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LSL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LYD", + "name": "Libyan dinar", + "symbol": "ل.د", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LYD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MAD", + "name": "Moroccan dirham", + "symbol": "د.م.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MAD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MDL", + "name": "Moldovan leu", + "symbol": "MDL", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MDL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MGA", + "name": "Malagasy ariary", + "symbol": "Ar", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MGA") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MKD", + "name": "Macedonian denar", + "symbol": "ден", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MKD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MMK", + "name": "Burmese kyat", + "symbol": "Ks", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MMK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MNT", + "name": "Mongolian tögrög", + "symbol": "₮", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MNT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MOP", + "name": "Macanese pataca", + "symbol": "P", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MOP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MRU", + "name": "Mauritanian ouguiya", + "symbol": "UM", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MRU") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MUR", + "name": "Mauritian rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MUR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MVR", + "name": "Maldivian rufiyaa", + "symbol": ".ރ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MVR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MWK", + "name": "Malawian kwacha", + "symbol": "MK", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MWK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MXN", + "name": "Mexican peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MXN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MYR", + "name": "Malaysian ringgit", + "symbol": "RM", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MYR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MZN", + "name": "Mozambican metical", + "symbol": "MT", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MZN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NAD", + "name": "Namibian dollar", + "symbol": "N$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NAD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NGN", + "name": "Nigerian naira", + "symbol": "₦", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NGN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NIO", + "name": "Nicaraguan córdoba", + "symbol": "C$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NIO") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NOK", + "name": "Norwegian krone", + "symbol": "kr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NOK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NPR", + "name": "Nepalese rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NPR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NZD", + "name": "New Zealand dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NZD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "OMR", + "name": "Omani rial", + "symbol": "ر.ع.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/OMR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PAB", + "name": "Panamanian balboa", + "symbol": "B/.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PAB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PEN", + "name": "Sol", + "symbol": "S/", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PEN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PGK", + "name": "Papua New Guinean kina", + "symbol": "K", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PGK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PHP", + "name": "Philippine peso", + "symbol": "₱", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PHP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PKR", + "name": "Pakistani rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PKR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PLN", + "name": "Polish złoty", + "symbol": "zł", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PLN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PRB", + "name": "Transnistrian ruble", + "symbol": "р.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PRB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PYG", + "name": "Paraguayan guaraní", + "symbol": "₲", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PYG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "QAR", + "name": "Qatari riyal", + "symbol": "ر.ق", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/QAR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RON", + "name": "Romanian leu", + "symbol": "lei", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RON") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RSD", + "name": "Serbian dinar", + "symbol": "рсд", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RSD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RUB", + "name": "Russian ruble", + "symbol": "₽", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RUB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RWF", + "name": "Rwandan franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RWF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SAR", + "name": "Saudi riyal", + "symbol": "ر.س", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SAR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SBD", + "name": "Solomon Islands dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SBD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SCR", + "name": "Seychellois rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SCR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SDG", + "name": "Sudanese pound", + "symbol": "ج.س.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SDG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SEK", + "name": "Swedish krona", + "symbol": "kr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SEK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SGD", + "name": "Singapore dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SGD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SHP", + "name": "Saint Helena pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SHP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SLL", + "name": "Sierra Leonean leone", + "symbol": "Le", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SLL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SOS", + "name": "Somali shilling", + "symbol": "Sh", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SOS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SRD", + "name": "Surinamese dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SRD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SSP", + "name": "South Sudanese pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SSP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "STN", + "name": "São Tomé and Príncipe dobra", + "symbol": "Db", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/STN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SYP", + "name": "Syrian pound", + "symbol": "ل.س", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SYP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SZL", + "name": "Swazi lilangeni", + "symbol": "E", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SZL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "THB", + "name": "Thai baht", + "symbol": "฿", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/THB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TJS", + "name": "Tajikistani somoni", + "symbol": "ЅМ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TJS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TMT", + "name": "Turkmenistan manat", + "symbol": "m", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TMT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TND", + "name": "Tunisian dinar", + "symbol": "د.ت", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TND") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TOP", + "name": "Tongan paʻanga", + "symbol": "T$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TOP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TRY", + "name": "Turkish lira", + "symbol": "₺", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TRY") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TTD", + "name": "Trinidad and Tobago dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TTD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TWD", + "name": "New Taiwan dollar", + "symbol": "NT$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TWD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TZS", + "name": "Tanzanian shilling", + "symbol": "Sh", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TZS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UAH", + "name": "Ukrainian hryvnia", + "symbol": "₴", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UAH") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UGX", + "name": "Ugandan shilling", + "symbol": "UGX", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UGX") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "USD", + "name": "United States (US) dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/USD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UYU", + "name": "Uruguayan peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UYU") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UZS", + "name": "Uzbekistani som", + "symbol": "UZS", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UZS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VEF", + "name": "Venezuelan bolívar", + "symbol": "Bs F", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VEF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VES", + "name": "Bolívar soberano", + "symbol": "Bs.S", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VES") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VND", + "name": "Vietnamese đồng", + "symbol": "₫", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VND") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VUV", + "name": "Vanuatu vatu", + "symbol": "Vt", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VUV") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "WST", + "name": "Samoan tālā", + "symbol": "T", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/WST") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XAF", + "name": "Central African CFA franc", + "symbol": "CFA", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XAF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XCD", + "name": "East Caribbean dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XCD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XOF", + "name": "West African CFA franc", + "symbol": "CFA", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XOF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XPF", + "name": "CFP franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XPF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "YER", + "name": "Yemeni rial", + "symbol": "﷼", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/YER") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZAR", + "name": "South African rand", + "symbol": "R", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ZAR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZMW", + "name": "Zambian kwacha", + "symbol": "ZK", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ZMW") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + }); + + test('can view currency data', async ({ + request + }) => { + // call API to retrieve a specific currency data + const response = await request.get('/wp-json/wc/v3/data/currencies/fkp'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "FKP", + "name": "Falkland Islands pound", + "symbol": "£" + })); + }); + + test('can view current currency', async ({ + request + }) => { + // call API to retrieve current currency data + const response = await request.get('/wp-json/wc/v3/data/currencies/current'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "USD", + "name": "United States (US) dollar", + "symbol": "$", + })); + }); +}); From 20bb576fb605b4ff4bb1ff12c4b4713ffc66453f Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Mon, 31 Oct 2022 12:29:43 -0700 Subject: [PATCH 104/149] Restore previously installed pnpm version (#35389) --- tools/code-analyzer/src/lib/scan-changes.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/code-analyzer/src/lib/scan-changes.ts b/tools/code-analyzer/src/lib/scan-changes.ts index 5042db58785..4ed137f97d3 100644 --- a/tools/code-analyzer/src/lib/scan-changes.ts +++ b/tools/code-analyzer/src/lib/scan-changes.ts @@ -70,6 +70,9 @@ export const scanForChanges = async ( ); const packageJSON = JSON.parse( fileStr ); + // Temporarily save the current PNPM version. + await execAsync( `tmpgPNPM="$(pnpm --version)"` ); + if ( packageJSON.engines && packageJSON.engines.pnpm ) { await execAsync( `npm i -g pnpm@${ packageJSON.engines.pnpm }`, @@ -101,6 +104,9 @@ export const scanForChanges = async ( schemaChanges = schemaDiff || []; + // Restore the previously saved PNPM version + await execAsync( `npm i -g pnpm@"$tmpgPNPM"` ); + Logger.endTask(); } From 1b2a94b030b8ca465a8fbcd76350d02c076e5eee Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Mon, 31 Oct 2022 20:01:21 +0000 Subject: [PATCH 105/149] add/a2p reports api-core-tests (#35388) add reports api-core-tests --- .../add-api-core-tests-reports-crud-tests | 4 + .../payment-gateways-crud.test.js | 3 - .../tests/reports/reports-crud.test.js | 407 ++++++++++++++++++ 3 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests create mode 100644 plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js diff --git a/plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests new file mode 100644 index 00000000000..ded7697523f --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for reports crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js index fb7ef68bf0a..f1e83fe7dba 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js @@ -2,9 +2,6 @@ const { test, expect } = require('@playwright/test'); -const { - refund -} = require('../../data'); /** * Tests for the WooCommerce Refunds API. diff --git a/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js new file mode 100644 index 00000000000..2766df5af22 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js @@ -0,0 +1,407 @@ +const { + test, + expect +} = require('@playwright/test'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group reports + * + */ +test.describe('Reports API tests', () => { + + test('can view all reports', async ({ + request + }) => { + // call API to retrieve the reports + const response = await request.get('/wp-json/wc/v3/reports'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + }); + + test('can view sales reports', async ({ + request + }) => { + // call API to retrieve the sales reports + const response = await request.get('/wp-json/wc/v3/reports/sales'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + const today = new Date(); + const dd = String(today.getDate()).padStart(2, '0'); + const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! + const yyyy = today.getFullYear(); + const dateString = yyyy + '-' + mm + '-' + dd; + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "total_sales": expect.any(String), + "net_sales": expect.any(String), + "average_sales": expect.any(String), + "total_orders": expect.any(Number), + "total_items": expect.any(Number), + "total_tax": expect.any(String), + "total_shipping": expect.any(String), + "total_refunds": expect.any(Number), + "total_discount": expect.any(String), + "totals_grouped_by": "day", + "totals": expect.objectContaining({ + [dateString]: { + "sales": expect.any(String), + "orders": expect.any(Number), + "items": expect.any(Number), + "tax": expect.any(String), + "shipping": expect.any(String), + "discount": expect.any(String), + "customers": expect.any(Number) + } + }), + "total_customers": expect.any(Number), + }) + ])); + }); + + test('can view top sellers reports', async ({ + request + }) => { + // call API to retrieve the top sellers + const response = await request.get('/wp-json/wc/v3/reports/top_sellers'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([])); + }); + + test('can view coupons totals', async ({ + request + }) => { + // call API to retrieve the coupons totals + const response = await request.get('/wp-json/wc/v3/reports/coupons/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "percent", + "name": "Percentage discount", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "fixed_cart", + "name": "Fixed cart discount", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "fixed_product", + "name": "Fixed product discount", + "total": expect.any(Number) + }) + ])); + }); + + test('can view customers totals', async ({ + request + }) => { + // call API to retrieve the customers totals + const response = await request.get('/wp-json/wc/v3/reports/customers/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "paying", + "name": "Paying customer", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "non_paying", + "name": "Non-paying customer", + "total": expect.any(Number) + }) + ])); + }); + + test('can view orders totals', async ({ + request + }) => { + // call API to retrieve the orders totals + const response = await request.get('/wp-json/wc/v3/reports/orders/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "pending", + "name": "Pending payment", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "processing", + "name": "Processing", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "on-hold", + "name": "On hold", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "completed", + "name": "Completed", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "cancelled", + "name": "Cancelled", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "refunded", + "name": "Refunded", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "failed", + "name": "Failed", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "checkout-draft", + "name": "Draft", + "total": expect.any(Number) + }) + ])); + }); + + test('can view products totals', async ({ + request + }) => { + // call API to retrieve the products totals + const response = await request.get('/wp-json/wc/v3/reports/products/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "external", + "name": "External/Affiliate product", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "grouped", + "name": "Grouped product", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "simple", + "name": "Simple product", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "variable", + "name": "Variable product", + "total": expect.any(Number) + }) + ])); + }); + + test('can view reviews totals', async ({ + request + }) => { + // call API to retrieve the reviews totals + const response = await request.get('/wp-json/wc/v3/reports/reviews/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_1_out_of_5", + "name": "Rated 1 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_2_out_of_5", + "name": "Rated 2 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_3_out_of_5", + "name": "Rated 3 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_4_out_of_5", + "name": "Rated 4 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_5_out_of_5", + "name": "Rated 5 out of 5", + "total": expect.any(Number) + }) + ])); + }); +}); From aea554bcdb5d248ebbbcbee230bbc2ca17017dc4 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Mon, 31 Oct 2022 17:11:31 -0300 Subject: [PATCH 106/149] Fix/form ts error (#35394) * Fix forwardRef type in the Form component * Add changelog * Revert type changes --- packages/js/components/changelog/fix-form_ts_error | 4 ++++ packages/js/components/src/form/form-context.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 packages/js/components/changelog/fix-form_ts_error diff --git a/packages/js/components/changelog/fix-form_ts_error b/packages/js/components/changelog/fix-form_ts_error new file mode 100644 index 00000000000..fc0ec73f0ee --- /dev/null +++ b/packages/js/components/changelog/fix-form_ts_error @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update variable name within useFormContext. diff --git a/packages/js/components/src/form/form-context.ts b/packages/js/components/src/form/form-context.ts index be43a98a9d6..d7ca500810d 100644 --- a/packages/js/components/src/form/form-context.ts +++ b/packages/js/components/src/form/form-context.ts @@ -58,7 +58,7 @@ export const FormContext = createContext< FormContext< any > >( // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useFormContext< Values extends Record< string, any > >() { - const formik = useContext< FormContext< Values > >( FormContext ); + const formContext = useContext< FormContext< Values > >( FormContext ); - return formik; + return formContext; } From 2355822ec5846d8895585327005c0bc775fd7255 Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:12:14 -0700 Subject: [PATCH 107/149] Ensure the HPOS/COT order status correctly tracks the CPT order status (#35402) Ensure the HPOS/COT order status correctly tracks the CPT order status (during manual order creation). * Linting. * Address coding standards (no need to prepare the query when there are no placeholders). * Woops! Restore use of `$wpdb->prepare()`, add phpcs:ignore rule. --- .../fix-35360-sync-on-manual-order-creation | 4 ++ .../DataStores/Orders/DataSynchronizer.php | 5 +- .../Orders/DataSynchronizerTests.php | 46 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation diff --git a/plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation b/plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation new file mode 100644 index 00000000000..c95d2cefa8a --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +When HPOS is enabled, posts are authoritative, and sync is enabled, ensure the HPOS record correctly tracks the CPT order record. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 39b9b02ca02..b4849201ae2 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -68,6 +68,7 @@ class DataSynchronizer implements BatchProcessorInterface { */ public function __construct() { self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 ); + self::add_action( 'woocommerce_new_order', array( $this, 'handle_updated_order' ), 100 ); self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 ); self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 ); } @@ -255,7 +256,9 @@ SELECT( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared switch ( $type ) { case self::ID_TYPE_MISSING_IN_ORDERS_TABLE: - $sql = $wpdb->prepare(" + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared. + $sql = $wpdb->prepare( + " SELECT posts.ID FROM $wpdb->posts posts LEFT JOIN $orders_table orders ON posts.ID = orders.id WHERE diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php index f073b7c8076..e64999ec1f9 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php @@ -146,4 +146,50 @@ class DataSynchronizerTests extends WC_Unit_Test_Case { 'The order was successfully copied to the COT table, outside of a dedicated synchronization batch.' ); } + + /** + * When sync is enbabled and the posts store is authoritative, creating an order and updating the status from + * draft to some non-draft status (as happens when an order is manually created in the admin environment) should + * result in the same status change being made in the duplicate COT record. + */ + public function test_status_syncs_correctly_after_order_creation() { + global $wpdb; + $orders_table = OrdersTableDataStore::get_orders_table_name(); + + // Enable sync, make the posts table authoritative. + update_option( $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, 'yes' ); + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + + // When a new order is manually created in the admin environment, WordPress automatically creates an empty + // draft post for us. + $order_id = (int) wp_insert_post( + array( + 'post_type' => 'shop_order', + 'post_status' => 'draft', + ) + ); + + // Once the admin user decides to go ahead and save the order (ie, they click 'Create'), we start performing + // various updates to the order record. + wc_get_order( $order_id )->save(); + + // As soon as the order is saved via our own internal API, the DataSynchronizer should create a copy of the + // record in the COT table. + $this->assertEquals( + 'draft', + $wpdb->get_var( "SELECT status FROM $orders_table WHERE id = $order_id" ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'When HPOS is enabled but the posts data store is authoritative, saving an order will result in a duplicate with the same status being saved in the COT table.' + ); + + // In a separate operation, the status will be updated to an actual non-draft order status. This should also be + // observed by the DataSynchronizer and a further update made to the COT table. + $order = wc_get_order( $order_id ); + $order->set_status( 'pending' ); + $order->save(); + $this->assertEquals( + 'wc-pending', + $wpdb->get_var( "SELECT status FROM $orders_table WHERE id = $order_id" ), //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'When the order status is updated, the change should be observed by the DataSynhronizer and a matching update will take place in the COT table.' + ); + } } From 66370c823f31865040e7863374b7425415afed4e Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Mon, 31 Oct 2022 14:36:33 -0700 Subject: [PATCH 108/149] Add inventory advanced section (#35164) * Add sold individually inventory option * Add backorder purchase options * Add margin around collapsible content areas * Add changelog entry * Add tests around inventory section * Fix up checkbox props after rebase * Check for disabled track quantity toggle * Update manage stock test * Fix nested radio control label margin --- .../products/layout/product-form-layout.scss | 3 +- .../layout/product-section-layout.scss | 4 + .../advanced-stock-section.tsx | 64 +++++++ .../product-inventory-section.tsx | 14 +- .../test/product-inventory-section.spec.tsx | 176 ++++++++++++++++++ plugins/woocommerce/changelog/add-34389 | 4 + 6 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx create mode 100644 plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx create mode 100644 plugins/woocommerce/changelog/add-34389 diff --git a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss index 06772f0b89b..10288998954 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss @@ -11,7 +11,8 @@ text-transform: none; } - .components-card__body h4:first-child { + .components-card__body h4:first-child, + .components-radio-control:first-child .components-base-control__label { margin-top: 0; } diff --git a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss index 87bb136736c..15b02e61f1f 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss @@ -32,6 +32,10 @@ .components-radio-control .components-v-stack { gap: $gap-small; } + + .woocommerce-collapsible-content { + margin-top: $gap-large; + } } &__header { diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx new file mode 100644 index 00000000000..957d31a84b8 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CheckboxControl, RadioControl } from '@wordpress/components'; +import { Product } from '@woocommerce/data'; +import { useFormContext } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { getCheckboxTracks } from '../utils'; + +export const AdvancedStockSection: React.FC = () => { + const { getCheckboxControlProps, getInputProps, values } = + useFormContext< Product >(); + + const backordersProp = getInputProps( 'backorders' ); + // These properties cause issues with the RadioControl component. + // A fix to form upstream would help if we can identify what type of input is used. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete backordersProp.checked; + delete backordersProp.value; + + return ( + <> + { values.manage_stock && ( + + ) } +

{ __( 'Restrictions', 'woocommerce' ) }

+ + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx index 74a1e23d98f..a7d2c01b5a1 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx @@ -3,14 +3,11 @@ */ import { __ } from '@wordpress/i18n'; import { + CollapsibleContent, __experimentalConditionalWrapper as ConditionalWrapper, Link, useFormContext, } from '@woocommerce/components'; -import { cloneElement } from '@wordpress/element'; -import { getAdminLink } from '@woocommerce/settings'; -import { Product } from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; import { Card, CardBody, @@ -18,10 +15,14 @@ import { TextControl, Tooltip, } from '@wordpress/components'; +import { getAdminLink } from '@woocommerce/settings'; +import { Product } from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ +import { AdvancedStockSection } from './advanced-stock-section'; import { getCheckboxTracks } from '../utils'; import { getAdminSetting } from '~/utils/admin-settings'; import { ProductSectionLayout } from '../../layout/product-section-layout'; @@ -109,6 +110,11 @@ export const ProductInventorySection: React.FC = () => { ) : ( ) } + + + diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx new file mode 100644 index 00000000000..a3c80a454b5 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx @@ -0,0 +1,176 @@ +/** + * External dependencies + */ +import { Form } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import { getAdminSetting } from '~/utils/admin-settings'; +import { ProductInventorySection } from '../'; + +jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); +jest.mock( '~/utils/admin-settings', () => ( { + getAdminSetting: jest.fn(), +} ) ); + +describe( 'ProductInventorySection', () => { + beforeEach( () => { + jest.clearAllMocks(); + ( getAdminSetting as jest.Mock ).mockImplementation( + ( key, value = false ) => { + const values = { + manageStock: 'yes', + notifyLowStockAmount: 5, + }; + if ( values.hasOwnProperty( key ) ) { + return values[ key as keyof typeof values ]; + } + return value; + } + ); + } ); + + const product: Partial< Product > = { + id: 1, + name: 'Lorem', + slug: 'lorem', + manage_stock: false, + }; + + it( 'should render the sku field', () => { + render( +
+ + + ); + + expect( + screen.getByLabelText( 'SKU (Stock Keeping Unit)' ) + ).toBeInTheDocument(); + } ); + + it( 'should disable the manage stock section if inventory management is turned off', () => { + ( getAdminSetting as jest.Mock ).mockImplementation( ( key, value ) => { + const values = { + manageStock: 'no', + }; + if ( values.hasOwnProperty( key ) ) { + return values[ key as keyof typeof values ]; + } + return value; + } ); + + render( +
+ + + ); + + expect( + screen.getByText( 'Track quantity for this product' ) + .previousSibling + ).toHaveClass( 'is-disabled' ); + } ); + + it( 'should not disable the manage stock section if inventory management is turned on', () => { + render( +
+ + + ); + + expect( + screen.getByText( 'Track quantity for this product' ) + .previousSibling + ).not.toHaveClass( 'is-disabled' ); + } ); + + it( 'should render the quantity field when product stock is being managed', () => { + render( +
+ + + ); + + expect( + screen.getByLabelText( 'Current quantity' ) + ).toBeInTheDocument(); + } ); + + it( 'should not render the quantity field when product stock is not being managed', () => { + render( +
+ + + ); + + expect( + screen.queryByLabelText( 'Current quantity' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should render the default low stock amount in placeholder', () => { + render( +
+ + + ); + + expect( + screen.getByPlaceholderText( '5 (store default)' ) + ).toBeInTheDocument(); + } ); + + it( 'should not render the advanced section until clicked', () => { + render( +
+ + + ); + + expect( + screen.queryByLabelText( 'Limit purchases to 1 item per order' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should render the advanced section after clicked', () => { + render( +
+ + + ); + + userEvent.click( screen.getByText( 'Advanced' ) ); + expect( + screen.getByLabelText( 'Limit purchases to 1 item per order' ) + ).toBeInTheDocument(); + } ); + + it( 'should not allow backorder settings when not managing stock', () => { + render( +
+ + + ); + + userEvent.click( screen.getByText( 'Advanced' ) ); + expect( + screen.queryByText( 'When out of stock' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should allow backorder settings when managing stock', () => { + render( +
+ + + ); + + userEvent.click( screen.getByText( 'Advanced' ) ); + expect( screen.queryByText( 'When out of stock' ) ).toBeInTheDocument(); + } ); +} ); diff --git a/plugins/woocommerce/changelog/add-34389 b/plugins/woocommerce/changelog/add-34389 new file mode 100644 index 00000000000..abd13470fa6 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34389 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product inventory advanced section From e1ebabba29082e24256ceca629cc19779820773c Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Mon, 31 Oct 2022 14:36:54 -0700 Subject: [PATCH 109/149] Fix up rich text editor initial selection and add blocks (#35286) * Fix double click toolbar behavior * Fix initial block selection on editor load * Add placeholder option to RichTextEditor * Add image and video blocks * Set toolbar height * Allow inserter to be shown * Allow media uploads in rich text editor * Add changelog entries * Fix media upload * Check for existence of selected blocks before checking length * Pass blocks to avoid race in detecting initially empty blocks --- .../changelog/fix-rich-text-editor-selection | 4 ++ .../rich-text-editor/editor-writing-flow.tsx | 41 +++++++++++------- .../src/rich-text-editor/rich-text-editor.tsx | 42 ++++++++++++++----- .../src/rich-text-editor/style.scss | 30 ++++++------- .../rich-text-editor/utils/register-blocks.ts | 4 ++ .../sections/product-details-section.tsx | 4 ++ .../changelog/fix-rich-text-editor-selection | 4 ++ 7 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 packages/js/components/changelog/fix-rich-text-editor-selection create mode 100644 plugins/woocommerce/changelog/fix-rich-text-editor-selection diff --git a/packages/js/components/changelog/fix-rich-text-editor-selection b/packages/js/components/changelog/fix-rich-text-editor-selection new file mode 100644 index 00000000000..59caa9a16d8 --- /dev/null +++ b/packages/js/components/changelog/fix-rich-text-editor-selection @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Fix up initial block selection in RichTextEditor and add media blocks diff --git a/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx b/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx index b56a6ef08fd..2734c566484 100644 --- a/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx +++ b/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx @@ -3,8 +3,8 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useInstanceId } from '@wordpress/compose'; +import { BlockInstance, createBlock } from '@wordpress/blocks'; import { createElement, useEffect } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; import { BlockList, ObserveTyping, @@ -15,37 +15,50 @@ import { WritingFlow, } from '@wordpress/block-editor'; -export const EditorWritingFlow: React.VFC = () => { +type EditorWritingFlowProps = { + blocks: BlockInstance[]; + onChange: ( changes: BlockInstance[] ) => void; + placeholder?: string; +}; + +export const EditorWritingFlow = ( { + blocks, + onChange, + placeholder = '', +}: EditorWritingFlowProps ) => { const instanceId = useInstanceId( EditorWritingFlow ); + const firstBlock = blocks[ 0 ]; + const isEmpty = ! blocks.length; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This action is available in the block editor data store. - const { insertBlock } = useDispatch( blockEditorStore ); - - const { isEmpty } = useSelect( ( select ) => { - const blocks = select( 'core/block-editor' ).getBlocks(); - + const { insertBlock, selectBlock } = useDispatch( blockEditorStore ); + const { selectedBlockClientIds } = useSelect( ( select ) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This selector is available in the block editor data store. const { getSelectedBlockClientIds } = select( blockEditorStore ); return { - isEmpty: blocks.length - ? blocks.length <= 1 && - blocks[ 0 ].attributes?.content?.trim() === '' - : true, - firstBlock: blocks[ 0 ], selectedBlockClientIds: getSelectedBlockClientIds(), }; } ); + useEffect( () => { + if ( selectedBlockClientIds?.length || ! firstBlock ) { + return; + } + selectBlock( firstBlock.clientId ); + }, [ firstBlock, selectedBlockClientIds ] ); + useEffect( () => { if ( isEmpty ) { const initialBlock = createBlock( 'core/paragraph', { content: '', + placeholder, } ); insertBlock( initialBlock ); + onChange( [ initialBlock ] ); } - }, [] ); + }, [ isEmpty ] ); return ( /* Gutenberg handles the keyboard events when focusing the content editable area. */ @@ -60,8 +73,6 @@ export const EditorWritingFlow: React.VFC = () => { - { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ } - { /* @ts-ignore This action is available in the block editor data store. */ } diff --git a/packages/js/components/src/rich-text-editor/rich-text-editor.tsx b/packages/js/components/src/rich-text-editor/rich-text-editor.tsx index d256503271c..3920a283970 100644 --- a/packages/js/components/src/rich-text-editor/rich-text-editor.tsx +++ b/packages/js/components/src/rich-text-editor/rich-text-editor.tsx @@ -1,18 +1,14 @@ /** * External dependencies */ -import { BaseControl, SlotFillProvider } from '@wordpress/components'; +import { BaseControl, Popover, SlotFillProvider } from '@wordpress/components'; import { BlockEditorProvider } from '@wordpress/block-editor'; import { BlockInstance } from '@wordpress/blocks'; +import { createElement, useEffect, useState, useRef } from '@wordpress/element'; import { debounce } from 'lodash'; -import { - createElement, - useCallback, - useEffect, - useState, - useRef, -} from '@wordpress/element'; import React from 'react'; +import { uploadMedia } from '@wordpress/media-utils'; +import { useUser } from '@woocommerce/data'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group @@ -31,15 +27,17 @@ type RichTextEditorProps = { label?: string; onChange: ( changes: BlockInstance[] ) => void; entryId?: string; + placeholder?: string; }; export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { blocks, label, onChange, + placeholder = '', } ) => { const blocksRef = useRef( blocks ); - + const { currentUserCan } = useUser(); const [ , setRefresh ] = useState( 0 ); // If there is a props change we need to update the ref and force re-render. @@ -61,6 +59,24 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { forceRerender(); }, 200 ); + const mediaUpload = currentUserCan( 'upload_files' ) + ? ( { + onError, + ...rest + }: { + onError: ( message: string ) => void; + } ) => { + uploadMedia( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The upload function passes the remaining required props. + { + onError: ( { message } ) => onError( message ), + ...rest, + } + ); + } + : undefined; + return (
{ label && ( @@ -75,13 +91,19 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This property was recently added in the block editor data store. __experimentalClearBlockSelection: false, + mediaUpload, } } onInput={ debounceChange } onChange={ debounceChange } > - + +
diff --git a/packages/js/components/src/rich-text-editor/style.scss b/packages/js/components/src/rich-text-editor/style.scss index 47b277c8075..722e98d31bf 100644 --- a/packages/js/components/src/rich-text-editor/style.scss +++ b/packages/js/components/src/rich-text-editor/style.scss @@ -1,3 +1,5 @@ +$toolbar-height: 40px; + .woocommerce-rich-text-editor { .woocommerce-rich-text-editor__writing-flow { border: 1px solid $gray-600; @@ -9,21 +11,12 @@ box-sizing: border-box; } - .block-editor-inserter { - display: none; - } - .block-editor-block-contextual-toolbar.is-fixed, .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar-group, .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar { border-color: $gray-600; } - /* Hide rich text placeholder text */ - .rich-text [data-rich-text-placeholder] { - display: none !important; - } - /* hide block boundary background styling */ .rich-text:focus *[data-rich-text-format-boundary] { background: none !important; @@ -39,11 +32,6 @@ outline: none; } - .block-editor-block-list__empty-block-inserter, - .block-editor-block-list__insertion-point { - display: none !important; - } - .block-editor-writing-flow { padding: $gap-small; } @@ -56,11 +44,25 @@ } .components-accessible-toolbar { + height: $toolbar-height; width: 100%; background-color: $white; border-color: $gray-700; border-bottom-left-radius: 0; border-bottom-right-radius: 0; + + .components-button { + height: $toolbar-height; + } + } + + .block-editor-block-mover:not(.is-horizontal) .block-editor-block-mover__move-button-container > * { + height: calc( $toolbar-height / 2 ); + } + + .block-editor-block-contextual-toolbar.is-fixed, + .components-toolbar-group { + min-height: $toolbar-height; } .wp-block-quote { diff --git a/packages/js/components/src/rich-text-editor/utils/register-blocks.ts b/packages/js/components/src/rich-text-editor/utils/register-blocks.ts index cdcf514835e..566ff239bef 100644 --- a/packages/js/components/src/rich-text-editor/utils/register-blocks.ts +++ b/packages/js/components/src/rich-text-editor/utils/register-blocks.ts @@ -14,6 +14,8 @@ export const HEADING_BLOCK_ID = 'core/heading'; export const LIST_BLOCK_ID = 'core/list'; export const LIST_ITEM_BLOCK_ID = 'core/list-item'; export const QUOTE_BLOCK_ID = 'core/quote'; +export const IMAGE_BLOCK_ID = 'core/image'; +export const VIDEO_BLOCK_ID = 'core/video'; const ALLOWED_CORE_BLOCKS = [ PARAGRAPH_BLOCK_ID, @@ -21,6 +23,8 @@ const ALLOWED_CORE_BLOCKS = [ LIST_BLOCK_ID, LIST_ITEM_BLOCK_ID, QUOTE_BLOCK_ID, + IMAGE_BLOCK_ID, + VIDEO_BLOCK_ID, ]; const registerCoreBlocks = () => { diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index 7f8bfd0b979..b452d8d3478 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -226,6 +226,10 @@ export const ProductDetailsSection: React.FC = () => { setDescriptionBlocks( blocks ); setValue( 'description', serialize( blocks ) ); } } + placeholder={ __( + 'Describe this product. What makes it unique? What are its most important features?', + 'woocommerce' + ) } /> diff --git a/plugins/woocommerce/changelog/fix-rich-text-editor-selection b/plugins/woocommerce/changelog/fix-rich-text-editor-selection new file mode 100644 index 00000000000..88bcd847753 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-rich-text-editor-selection @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add placeholder to description field From 8bc69e755e65a4cc1f0903c3b344107674ab8262 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:09:20 -0500 Subject: [PATCH 110/149] Delete changelog files based on PR 35428 (#35430) Delete changelog files for 35428 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/update-blocks-8.7.5 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/update-blocks-8.7.5 diff --git a/plugins/woocommerce/changelog/update-blocks-8.7.5 b/plugins/woocommerce/changelog/update-blocks-8.7.5 deleted file mode 100644 index e102e49b550..00000000000 --- a/plugins/woocommerce/changelog/update-blocks-8.7.5 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update WooCommerce Blocks to 8.7.5 From 6b770820e9e450d059d45f24c48a8950805790c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:14:47 -0500 Subject: [PATCH 111/149] Delete changelog files based on PR 35118 (#35433) Delete changelog files for 35118 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-35032 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35032 diff --git a/plugins/woocommerce/changelog/fix-35032 b/plugins/woocommerce/changelog/fix-35032 deleted file mode 100644 index eec41196f73..00000000000 --- a/plugins/woocommerce/changelog/fix-35032 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Sync orders for stats table. From 1f0d4df8ed79bf9811a604de5f45862fda98d710 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:17:19 -0500 Subject: [PATCH 112/149] Delete changelog files based on PR 35366 (#35435) Delete changelog files for 35366 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-35112 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35112 diff --git a/plugins/woocommerce/changelog/fix-35112 b/plugins/woocommerce/changelog/fix-35112 deleted file mode 100644 index 5375bc7a568..00000000000 --- a/plugins/woocommerce/changelog/fix-35112 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Allow line breaks in order note again. From f6f359e1cde0ece4e4b28c47d713e125b0b7bec2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:18:44 -0500 Subject: [PATCH 113/149] Delete changelog files based on PR 35402 (#35437) Delete changelog files for 35402 Co-authored-by: WooCommerce Bot --- .../changelog/fix-35360-sync-on-manual-order-creation | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation diff --git a/plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation b/plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation deleted file mode 100644 index c95d2cefa8a..00000000000 --- a/plugins/woocommerce/changelog/fix-35360-sync-on-manual-order-creation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -When HPOS is enabled, posts are authoritative, and sync is enabled, ensure the HPOS record correctly tracks the CPT order record. From a7a7b5187fa76963262f7889112409ef751a11ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:20:05 -0500 Subject: [PATCH 114/149] Delete changelog files based on PR 35333 (#35439) Delete changelog files for 35333 Co-authored-by: WooCommerce Bot --- .../remove_compatibility_warnings_for_inactive_plugins | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins diff --git a/plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins b/plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins deleted file mode 100644 index 2731dfabc35..00000000000 --- a/plugins/woocommerce/changelog/remove_compatibility_warnings_for_inactive_plugins +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Don't show feature compatibility warnings for inactive plugins From 24f6acdc9cb01b16e34bc4b68b66d476f33555ed Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Mon, 31 Oct 2022 17:46:29 -0500 Subject: [PATCH 115/149] Remove change file for 35349 (#35443) --- plugins/woocommerce/changelog/fix-order_type | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-order_type diff --git a/plugins/woocommerce/changelog/fix-order_type b/plugins/woocommerce/changelog/fix-order_type deleted file mode 100644 index ba0073e6737..00000000000 --- a/plugins/woocommerce/changelog/fix-order_type +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Check order type is set before returning to prevent notice. From ff2b06c9af06e7ba85ea9cdc261d8e7df78b4ebc Mon Sep 17 00:00:00 2001 From: louwie17 Date: Tue, 1 Nov 2022 09:05:46 -0300 Subject: [PATCH 116/149] Add create attribute term modal (#35131) * Add create attribute term modal * Add back filter missed during rebase * Add changelog * Fix lint error * Address some feedback from PR review * Prevent first modal from closing if closing the second modal when clicking outside --- .../attribute-field/add-attribute-modal.tsx | 11 +- .../attribute-term-input-field.scss | 8 + .../attribute-term-input-field.tsx | 280 ++++++++++++------ .../create-attribute-term-modal.scss | 22 ++ .../create-attribute-term-modal.tsx | 175 +++++++++++ .../add-34331_create_attribute_term_modal | 4 + 6 files changed, 404 insertions(+), 96 deletions(-) create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx create mode 100644 plugins/woocommerce/changelog/add-34331_create_attribute_term_modal diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx index 6ead6cd435e..effe7a7ed37 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx @@ -138,7 +138,16 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { return ( onClose( values ) } + onRequestClose={ ( + event: + | React.KeyboardEvent< Element > + | React.MouseEvent< Element > + | React.FocusEvent< Element > + ) => { + if ( ! event.isPropagationStopped() ) { + onClose( values ); + } + } } className="woocommerce-add-attribute-modal" > diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss index b4836d8ce10..a3509b2b7ef 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss @@ -2,4 +2,12 @@ &__loading-spinner { padding: 12px 0; } + &__add-new { + display: flex; + align-items: center; + font-weight: 600; + } + &__add-new-icon { + margin-right: $gap-small; + } } diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx index 5d69aced711..dfa6f30a6fc 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx @@ -1,11 +1,12 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { CheckboxControl, Spinner } from '@wordpress/components'; +import { sprintf, __ } from '@wordpress/i18n'; +import { CheckboxControl, Icon, Spinner } from '@wordpress/components'; import { resolveSelect } from '@wordpress/data'; -import { useCallback, useEffect, useState } from '@wordpress/element'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { useDebounce } from '@wordpress/compose'; +import { plus } from '@wordpress/icons'; import { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, ProductAttributeTerm, @@ -21,6 +22,7 @@ import { * Internal dependencies */ import './attribute-term-input-field.scss'; +import { CreateAttributeTermModal } from './create-attribute-term-modal'; type AttributeTermInputFieldProps = { value?: ProductAttributeTerm[]; @@ -30,13 +32,20 @@ type AttributeTermInputFieldProps = { disabled?: boolean; }; +let uniqueId = 0; + export const AttributeTermInputField: React.FC< AttributeTermInputFieldProps > = ( { value = [], onChange, placeholder, disabled, attributeId } ) => { + const attributeTermInputId = useRef( + `woocommerce-attribute-term-field-${ ++uniqueId }` + ); const [ fetchedItems, setFetchedItems ] = useState< ProductAttributeTerm[] >( [] ); const [ isFetching, setIsFetching ] = useState( false ); + const [ addNewAttributeTermName, setAddNewAttributeTermName ] = + useState< string >(); const fetchItems = useCallback( ( searchString: string | undefined ) => { @@ -80,6 +89,11 @@ export const AttributeTermInputField: React.FC< }; const onSelect = ( item: ProductAttributeTerm ) => { + // Add new item. + if ( item.id === -99 ) { + setAddNewAttributeTermName( item.name ); + return; + } const isSelected = value.find( ( i ) => i.slug === item.slug ); if ( isSelected ) { onRemove( item ); @@ -88,103 +102,179 @@ export const AttributeTermInputField: React.FC< onChange( [ ...value, item ] ); }; + const focusSelectControl = () => { + const selectControlInputField: HTMLInputElement | null = + document.querySelector( + '.' + + attributeTermInputId.current + + ' .woocommerce-experimental-select-control__input' + ); + if ( selectControlInputField ) { + setTimeout( () => { + selectControlInputField.focus(); + }, 0 ); + } + }; + const selectedTermSlugs = ( value || [] ).map( ( term ) => term.slug ); return ( - - items={ fetchedItems } - multiple - disabled={ disabled || ! attributeId } - label="" - getFilteredItems={ ( allItems ) => allItems } - onInputChange={ debouncedSearch } - placeholder={ placeholder || '' } - getItemLabel={ ( item ) => item?.name || '' } - getItemValue={ ( item ) => item?.slug || '' } - stateReducer={ ( state, actionAndChanges ) => { - const { changes, type } = actionAndChanges; - switch ( type ) { - case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem: - return { - ...changes, - inputValue: state.inputValue, - }; - case selectControlStateChangeTypes.ItemClick: - return { - ...changes, - isOpen: true, - inputValue: state.inputValue, - highlightedIndex: state.highlightedIndex, - }; - default: - return changes; + <> + + items={ fetchedItems } + multiple + disabled={ disabled || ! attributeId } + label="" + getFilteredItems={ ( allItems, inputValue ) => { + if ( + inputValue.length > 0 && + ! allItems.find( + ( item ) => + item.name.toLowerCase() === + inputValue.toLowerCase() + ) + ) { + return [ + ...allItems, + { + id: -99, + name: inputValue, + } as ProductAttributeTerm, + ]; + } + return allItems; + } } + onInputChange={ debouncedSearch } + placeholder={ placeholder || '' } + getItemLabel={ ( item ) => item?.name || '' } + getItemValue={ ( item ) => item?.slug || '' } + stateReducer={ ( state, actionAndChanges ) => { + const { changes, type } = actionAndChanges; + switch ( type ) { + case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem: + return { + ...changes, + inputValue: state.inputValue, + }; + case selectControlStateChangeTypes.ItemClick: + if ( + changes.selectedItem && + changes.selectedItem.id === -99 + ) { + return changes; + } + return { + ...changes, + isOpen: true, + inputValue: state.inputValue, + highlightedIndex: state.highlightedIndex, + }; + default: + return changes; + } + } } + selected={ value } + onSelect={ onSelect } + onRemove={ onRemove } + className={ + 'woocommerce-attribute-term-field ' + + attributeTermInputId.current } - } } - selected={ value } - onSelect={ onSelect } - onRemove={ onRemove } - className="woocommerce-attribute-term-field" - > - { ( { - items, - highlightedIndex, - getItemProps, - getMenuProps, - isOpen, - } ) => { - return ( - - { [ - isFetching ? ( -
- -
- ) : null, - ...items.map( ( item, menuIndex ) => { - const isSelected = selectedTermSlugs.includes( - item.slug - ); - - return ( - + { ( { + items, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { [ + isFetching ? ( +
- <> - null } - checked={ isSelected } - label={ - - { item.name } + +
+ ) : null, + ...items.map( ( item, menuIndex ) => { + const isSelected = + selectedTermSlugs.includes( item.slug ); + + return ( + + { item.id !== -99 ? ( + null } + checked={ isSelected } + label={ + + { item.name } + + } + /> + ) : ( +
+ + + { sprintf( + /* translators: The name of the new attribute term to be created */ + __( + 'Create "%s"', + 'woocommerce' + ), + item.name + ) } - } - /> - - - ); - } ), - ].filter( - ( child ): child is JSX.Element => child !== null - ) } -
- ); - } } - +
+ ) } + + ); + } ), + ].filter( + ( child ): child is JSX.Element => + child !== null + ) } + + ); + } } +
+ { addNewAttributeTermName && attributeId !== undefined && ( + { + setAddNewAttributeTermName( undefined ); + focusSelectControl(); + } } + attributeId={ attributeId } + onCreated={ ( newAttribute ) => { + onSelect( newAttribute ); + setAddNewAttributeTermName( undefined ); + focusSelectControl(); + } } + /> + ) } + ); }; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss new file mode 100644 index 00000000000..87c014b3880 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss @@ -0,0 +1,22 @@ +.woocommerce-create-attribute-term-modal { + @mixin break-medium { + min-width: 650px; + } + + &__buttons { + margin-top: $gap-larger; + display: flex; + flex-direction: row; + gap: 8px; + justify-content: flex-end; + } + .has-error { + .components-base-control__help { + color: $studio-red-50; + } + } + .components-base-control:not(:first-child) { + margin-top: 16px; + margin-bottom: 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx new file mode 100644 index 00000000000..107b6ee6755 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx @@ -0,0 +1,175 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + Button, + Modal, + TextareaControl, + TextControl, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { cleanForSlug } from '@wordpress/url'; +import { Form, FormContext, FormErrors } from '@woocommerce/components'; +import { recordEvent } from '@woocommerce/tracks'; +import { + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, + ProductAttributeTerm, + QueryProductAttribute, +} from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import './create-attribute-term-modal.scss'; + +type CreateAttributeTermModalProps = { + initialAttributeTermName: string; + attributeId: number; + onCancel?: () => void; + onCreated?: ( newAttribute: ProductAttributeTerm ) => void; +}; + +export const CreateAttributeTermModal: React.FC< + CreateAttributeTermModalProps +> = ( { + initialAttributeTermName, + attributeId, + onCancel = () => {}, + onCreated = () => {}, +} ) => { + const { createNotice } = useDispatch( 'core/notices' ); + const [ isCreating, setIsCreating ] = useState( false ); + const { createProductAttributeTerm, invalidateResolutionForStoreSelector } = + useDispatch( EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME ); + + const onAdd = async ( attribute: Partial< ProductAttributeTerm > ) => { + recordEvent( 'product_attribute_term_add', { + new_product_page: true, + } ); + setIsCreating( true ); + try { + const newAttribute: ProductAttributeTerm = + await createProductAttributeTerm( { + ...attribute, + attribute_id: attributeId, + } ); + recordEvent( 'product_attribute_term_add_success', { + new_product_page: true, + } ); + invalidateResolutionForStoreSelector( 'getProductAttributes' ); + setIsCreating( false ); + onCreated( newAttribute ); + } catch ( e ) { + recordEvent( 'product_attribute_term_add_failed', { + new_product_page: true, + } ); + createNotice( + 'error', + __( 'Failed to create attribute term.', 'woocommerce' ) + ); + setIsCreating( false ); + onCancel(); + } + }; + + function validateForm( + values: Partial< ProductAttributeTerm > + ): FormErrors< ProductAttributeTerm > { + const errors: FormErrors< ProductAttributeTerm > = {}; + + if ( ! values.name?.length ) { + errors.name = __( + 'The attribute term name is required.', + 'woocommerce' + ); + } + + return errors; + } + + return ( + + | React.MouseEvent< Element > + | React.FocusEvent< Element > + ) => { + event.stopPropagation(); + onCancel(); + } } + className="woocommerce-create-attribute-term-modal" + > + > + initialValues={ { + name: initialAttributeTermName, + slug: cleanForSlug( initialAttributeTermName ), + } } + validate={ validateForm } + errors={ {} } + onSubmit={ onAdd } + > + { ( { + getInputProps, + handleSubmit, + isValidForm, + setValue, + values, + }: FormContext< QueryProductAttribute > ) => { + const nameInputProps = getInputProps< string >( 'name' ); + return ( + <> + { + nameInputProps.onBlur(); + setValue( + 'slug', + cleanForSlug( values.name ) + ); + } } + /> + + +
+ + +
+ + ); + } } + +
+ ); +}; diff --git a/plugins/woocommerce/changelog/add-34331_create_attribute_term_modal b/plugins/woocommerce/changelog/add-34331_create_attribute_term_modal new file mode 100644 index 00000000000..b297668b20a --- /dev/null +++ b/plugins/woocommerce/changelog/add-34331_create_attribute_term_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add option and modal to create new attribute terms within MVP attribute modal. From 9a7a75fdea393a8213f4b1cf8d0cbfe4388b9008 Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:55:29 +0000 Subject: [PATCH 117/149] add/a2p product variations api-core-tests (#35355) * add product variations api-core-tests * add product variations api-core-tests * comments updates --- ...i-core-tests-product-variations-crud-tests | 4 + .../tests/products/products-crud.test.js | 206 ++++++++++++++++-- 2 files changed, 186 insertions(+), 24 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests diff --git a/plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests new file mode 100644 index 00000000000..56315464db6 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for product variations crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js index efdf05f6fd4..098f686712a 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js @@ -405,7 +405,7 @@ test.describe('Products API tests: CRUD', () => { test('can retrieve all product categories', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product categories const response = await request.get('/wp-json/wc/v3/products/categories'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); @@ -417,7 +417,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product category', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product categories const response = await request.put(`wp-json/wc/v3/products/categories/${productCategoryId}`, { data: { description: 'Games played on a video games console or computer.' @@ -611,7 +611,7 @@ test.describe('Products API tests: CRUD', () => { test('can retrieve all product reviews', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product reviews const response = await request.get('/wp-json/wc/v3/products/reviews'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); @@ -622,7 +622,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product review', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product reviews const response = await request.put(`wp-json/wc/v3/products/reviews/${productReviewId}`, { data: { rating: 1 @@ -644,7 +644,7 @@ test.describe('Products API tests: CRUD', () => { test('can permanently delete a product review', async ({ request }) => { - // Delete the product category. + // Delete the product review. const response = await request.delete( `wp-json/wc/v3/products/reviews/${productReviewId}`, { data: { @@ -801,7 +801,7 @@ test.describe('Products API tests: CRUD', () => { test('can retrieve all product shipping classes', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product shipping classes const response = await request.get('/wp-json/wc/v3/products/shipping_classes'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); @@ -812,7 +812,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product shipping class', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve a product shipping class const response = await request.put(`wp-json/wc/v3/products/shipping_classes/${productShippingClassId}`, { data: { description: 'This is a description for the Priority shipping class.' @@ -959,7 +959,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product tag', async ({ request }) => { - // call API to retrieve all product tags + // call API to update a product tag const response = await request.put(`wp-json/wc/v3/products/tags/${productTagId}`, { data: { description: 'Genuine leather.' @@ -1098,25 +1098,183 @@ test.describe('Products API tests: CRUD', () => { }); }); - test('can add a variable product', async ({ - request - }) => { - const response = await request.post('wp-json/wc/v3/products', { - data: variableProduct, + test.describe('Product variation tests: CRUD', () => { + let variableProductId; + let productVariationId; + + test('can add a variable product', async ({ + request + }) => { + const response = await request.post('wp-json/wc/v3/products', { + data: variableProduct, + }); + const responseJSON = await response.json(); + variableProductId = responseJSON.id; + expect(response.status()).toEqual(201); + expect(typeof variableProductId).toEqual('number'); + expect(responseJSON).toMatchObject(variableProduct); + expect(responseJSON.status).toEqual('publish'); }); - const responseJSON = await response.json(); - const variableProductId = responseJSON.id; - expect(response.status()).toEqual(201); - expect(typeof variableProductId).toEqual('number'); - expect(responseJSON).toMatchObject(variableProduct); - expect(responseJSON.status).toEqual('publish'); + test('can add a product variation', async ({ + request + }) => { + const response = await request.post(`wp-json/wc/v3/products/${variableProductId}/variations`, { + data: { + "regular_price": "29.00", + "attributes": [{ + "name": "Colour", + "option": "Green" + }] + }, + }); + const responseJSON = await response.json(); + productVariationId = responseJSON.id; + expect(response.status()).toEqual(201); + expect(typeof productVariationId).toEqual('number'); + expect(responseJSON.regular_price).toEqual("29.00"); + }); - // Cleanup: Delete the variable product - await request.delete(`wp-json/wc/v3/products/${ variableProductId }`, { - data: { - force: true, - }, + test('can retrieve a product variation', async ({ + request + }) => { + const response = await request.get(`wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}`); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.id).toEqual(productVariationId); + expect(responseJSON.regular_price).toEqual('29.00'); + }); + + test('can retrieve all product variations', async ({ + request + }) => { + // call API to retrieve all product variations + const response = await request.get(`wp-json/wc/v3/products/${variableProductId}/variations`); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toBeGreaterThan(0); + }); + + test('can update a product variation', async ({ + request + }) => { + // call API to update the product variation + const response = await request.put(`wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}`, { + data: { + "regular_price": "30.00", + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.id).toEqual(productVariationId); + expect(responseJSON.regular_price).toEqual('30.00'); + }); + + test('can permanently delete a product variation', async ({ + request + }) => { + // Delete the product variation. + const response = await request.delete( + `wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}`, { + data: { + force: true, + }, + } + ); + expect(response.status()).toEqual(200); + + // Verify that the product variation can no longer be retrieved. + const getDeletedProductVariationResponse = await request.get( + `wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}` + ); + expect(getDeletedProductVariationResponse.status()).toEqual(404); + }); + + test('can batch update product variations', async ({ + request + }) => { + // Batch create 2 product variations + const response = await request.post( + `wp-json/wc/v3/products/${variableProductId}/variations/batch`, { + data: { + create: [{ + "regular_price": "30.00", + "attributes": [{ + "name": "Colour", + "option": "Green" + }] + }, + { + "regular_price": "35.00", + "attributes": [{ + "name": "Colour", + "option": "Red" + }] + } + ] + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + const variation1Id = responseJSON.create[0].id; + const variation2Id = responseJSON.create[1].id; + expect(typeof variation1Id).toEqual('number'); + expect(typeof variation2Id).toEqual('number'); + expect(responseJSON.create[0].price).toEqual('30.00'); + expect(responseJSON.create[1].price).toEqual('35.00'); + + // Batch create a new variation, update a variation and delete another. + const responseBatchUpdate = await request.post( + `wp-json/wc/v3/products/${variableProductId}/variations/batch`, { + data: { + create: [{ + "regular_price": "25.99", + "attributes": [{ + "name": "Colour", + "option": "Blue" + }] + }], + update: [{ + id: variation2Id, + "regular_price": "35.99", + }], + delete: [ + variation1Id + ] + } + } + ); + + expect(response.status()).toEqual(200); + const responseBatchUpdateJSON = await responseBatchUpdate.json(); + const variation3Id = responseBatchUpdateJSON.create[0].id; + const responseUpdatedVariation = await request.get(`wp-json/wc/v3/products/${variableProductId}/variations/${variation2Id}`); + const responseUpdatedVariationJSON = await responseUpdatedVariation.json(); + expect(responseUpdatedVariationJSON.regular_price).toEqual('35.99'); + + // Verify that the deleted product variation can no longer be retrieved. + const getDeletedProductVariationResponse = await request.get( + `wp-json/wc/v3/products/${variableProductId}/variations/${variation1Id}` + ); + expect(getDeletedProductVariationResponse.status()).toEqual(404); + + // Batch delete the created product variations + await request.post( + `wp-json/wc/v3/products/${variableProductId}/variations/batch`, { + data: { + delete: [variation2Id, variation3Id] + } + } + ); + + // Cleanup: Delete the variable product + await request.delete(`wp-json/wc/v3/products/${ variableProductId }`, { + data: { + force: true, + }, + }); }); }); From c7282de4eeb2b5c9b92a4681f7ec9fbe43fe68c6 Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Tue, 1 Nov 2022 14:04:20 +0000 Subject: [PATCH 118/149] update/a2p update shipping api-core-tests (#35332) * update shipping api-core-tests * merge in trunk to see if this resolves changelog issue * comments updates --- .../update-api-core-tests-shipping-crud-tests | 4 + .../tests/shipping/shipping-method.test.js | 111 +++++++++++++++++- .../tests/shipping/shipping-zones.test.js | 11 -- 3 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests diff --git a/plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests b/plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests new file mode 100644 index 00000000000..cff840b283b --- /dev/null +++ b/plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update playwright api-core-tests for shipping crud operations \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js index d3c038b9ca8..5a66200f908 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js @@ -1,7 +1,12 @@ /* eslint-disable */ -const { test, expect } = require('@playwright/test'); +const { + test, + expect +} = require('@playwright/test'); -const { getShippingMethodExample } = require('../../data'); +const { + getShippingMethodExample +} = require('../../data'); /** * Shipping zone id for "Locations not covered by your other zones". @@ -30,11 +35,107 @@ const methodCostIndex = 2; */ test.describe('Shipping methods API tests', () => { + test('cannot create a shipping method', async ({ + request, + }) => { + /** + * call API to attempt to create a shipping method + * This call will not work as we have no ability to create new shipping methods, + * only retrieve the existing shipping methods + * i.e. Flat rate, Free shipping and Local pickup + */ + const response = await request.post( + '/wp-json/wc/v3/shipping_methods', { + data: { + title: "flat_rate", + description: "Lets you charge a fixed rate for shipping.", + }, + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual('rest_no_route'); + expect(responseJSON.message).toEqual('No route was found matching the URL and request method.'); + }); + + test('can retrieve all shipping methods', async ({ + request + }) => { + // call API to retrieve all shipping methods + const response = await request.get('/wp-json/wc/v3/shipping_methods'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)); + expect(responseJSON.length).toEqual(3); + expect(responseJSON[0].id).toEqual("flat_rate"); + expect(responseJSON[1].id).toEqual("free_shipping"); + expect(responseJSON[2].id).toEqual("local_pickup"); + }); + + test('can retrieve a shipping method', async ({ + request + }) => { + // call API to retrieve a shipping method + const response = await request.get( + `/wp-json/wc/v3/shipping_methods/local_pickup` + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + expect(typeof responseJSON.id).toEqual('string'); + }); + + test(`cannot update a shipping method`, async ({ + request, + }) => { + /** + * call API to attempt to update a shipping method + * This call will not work as we have no ability to update new shipping methods, + * only retrieve the existing shipping methods + * i.e. Flat rate, Free shipping and Local pickup + */ + const response = await request.put( + '/wp-json/wc/v3/shipping_methods/local_pickup', { + data: { + description: "update local pickup description" + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual('rest_no_route'); + expect(responseJSON.message).toEqual('No route was found matching the URL and request method.'); + }); + + + test('cannot delete a shipping method', async ({ + request + }) => { + /** + * call API to attempt to delete a shipping method + * This call will not work as we have no ability to delete shipping methods, + * only retrieve the existing shipping methods + * i.e. Flat rate, Free shipping and Local pickup + */ + const response = await request.delete('/wp-json/wc/v3/shipping_methods', { + data: { + force: true + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual('rest_no_route'); + expect(responseJSON.message).toEqual('No route was found matching the URL and request method.'); + }); + + //loop through each row from the shippingMethods test data table above for (const shippingMethodRow of shippingMethods) { test(`can add a ${shippingMethodRow[methodTitleIndex]} shipping method`, - async ({ request }) => { + async ({ + request + }) => { //create the shipping method const shippingMethod = getShippingMethodExample(shippingMethodRow[methodIdIndex], shippingMethodRow[methodCostIndex]); @@ -54,12 +155,12 @@ test.describe('Shipping methods API tests', () => { const shippingMethodInstanceId = responseJSON.id; - //if the shipping method is flat_rate OR local_pickup then based on the data, it should have a cost value + // if the shipping method is flat_rate OR local_pickup then based on the data, it should have a cost value if (['flat_rate', 'local_pickup'].includes(shippingMethodRow[methodIdIndex])) { expect(responseJSON.settings.cost.value).toEqual(shippingMethodRow[methodCostIndex]); } - // Cleanup: Delete the shipping method + // Cleanup: Remove the shipping method from the shipping zone const deleteResponse = await request.delete(`/wp-json/wc/v3/shipping/zones/${ shippingZoneId }/methods/${ shippingMethodInstanceId }`, { data: { force: true diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js index 1316257c6f1..ef948dea1c4 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js @@ -121,7 +121,6 @@ test.describe( 'Shipping zones API tests', () => { //call API to retrive the locations of the last created shipping zone const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`); - const responseJSON = await response.json(); expect( response.status() ).toEqual( 200 ); //no locations exist initially @@ -142,11 +141,6 @@ test.describe( 'Shipping zones API tests', () => { test( 'can update a shipping region on a shipping zone', async ({request}) => { - //call API to retrive the locations of the last created shipping zone - const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - //GB and US locations exist initially //update the locations of the shipping zone regions to contain an individual state const putResponseStateOnly = await request.put( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`,{ @@ -166,11 +160,6 @@ test.describe( 'Shipping zones API tests', () => { test( 'can clear/delete a shipping region on a shipping zone', async ({request}) => { - //call API to retrive the locations of the last created shipping zone - const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - //GB and US locations exist initially //update the locations of the shipping zone regions to contain an individual state const putResponseStateOnly = await request.put( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`,{ From c2473da79be9333fa0c3f0a3e24008c429107ba3 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Tue, 1 Nov 2022 11:46:36 -0700 Subject: [PATCH 119/149] Remove insight first product and payment note (#35309) * Remove InsightFirstProductAndPayment note * Add changelog entry * Remove obsolete note * Fix yoda condition --- ...ove-insight-first-product-and-payment-note | 4 ++ .../woocommerce/includes/class-wc-install.php | 1 + .../woocommerce/src/Internal/Admin/Events.php | 4 +- .../Notes/InsightFirstProductAndPayment.php | 69 ------------------- 4 files changed, 6 insertions(+), 72 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note delete mode 100644 plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php diff --git a/plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note b/plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note new file mode 100644 index 00000000000..d14bff55fda --- /dev/null +++ b/plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove InsightFirstProductAndPayment note diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index d73908d3013..e9b8f015ed3 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -822,6 +822,7 @@ class WC_Install { global $wpdb; $obsolete_notes_names = array( 'wc-admin-welcome-note', + 'wc-admin-insight-first-product-and-payment', 'wc-admin-store-notice-setting-moved', 'wc-admin-store-notice-giving-feedback', 'wc-admin-learn-more-about-product-settings', diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index 58e1fb89586..94ed2c4f4d7 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -20,7 +20,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove; use \Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstDownlaodableProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct; -use \Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstProductAndPayment; use \Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstSale; use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use \Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist; @@ -84,7 +83,6 @@ class Events { EUVATNumber::class, FirstDownlaodableProduct::class, FirstProduct::class, - InsightFirstProductAndPayment::class, InsightFirstSale::class, LaunchChecklist::class, MagentoMigration::class, @@ -129,7 +127,7 @@ class Events { * @return object Instance. */ final public static function instance() { - if ( static::$instance === null ) { + if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php b/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php deleted file mode 100644 index 8d78705ac2f..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php +++ /dev/null @@ -1,69 +0,0 @@ -set_title( __( 'Insight', 'woocommerce' ) ); - $note->set_content( __( 'More than 80% of new merchants add the first product and have at least one payment method set up during the first week.

Do you find this type of insight useful?', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_SURVEY ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'affirm-insight-first-product-and-payment', - __( 'Yes', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - - $note->add_action( - 'affirm-insight-first-product-and-payment', - __( 'No', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - - return $note; - } -} From 2fcb28b5df0b42fb3f138fcdb2445a4ab9f7c10d Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:36:11 -0700 Subject: [PATCH 120/149] Add filter hook `handle_bulk_actions-woocommerce_page_wc-orders` (#35442) Add filter hook `handle_bulk_actions-woocommerce_page_wc-orders` to the HPOS admin list table. This is a duplicate of core WP hook `handle_bulk_actions-` and allows for custom bulk actions to be handled in the context of the admin list table for HPOS orders. --- .../changelog/fix-35414-custom-bulk-actions | 4 ++++ .../src/Internal/Admin/Orders/ListTable.php | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35414-custom-bulk-actions diff --git a/plugins/woocommerce/changelog/fix-35414-custom-bulk-actions b/plugins/woocommerce/changelog/fix-35414-custom-bulk-actions new file mode 100644 index 00000000000..8436c71c4a3 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35414-custom-bulk-actions @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Make it possible to add custom bulk action handling to the admin order list screen (when HPOS is enabled). diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php index c98f1090b33..c16c42482f5 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php @@ -941,9 +941,24 @@ class ListTable extends WP_List_Table { if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { $changed = $this->do_bulk_action_mark_orders( $ids, $new_status ); } + } else { + $screen = get_current_screen()->id; + + /** + * This action is documented in /wp-admin/edit.php (it is a core WordPress hook). + * + * @since 7.2.0 + * + * @param string $redirect_to The URL to redirect to after processing the bulk actions. + * @param string $action The current bulk action. + * @param int[] $ids IDs for the orders to be processed. + */ + $custom_sendback = apply_filters( "handle_bulk_actions-{$screen}", $redirect_to, $action, $ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } - if ( $changed ) { + if ( ! empty( $custom_sendback ) ) { + $redirect_to = $custom_sendback; + } elseif ( $changed ) { $redirect_to = add_query_arg( array( 'bulk_action' => $report_action, From dc94b9b512b3273cc585aa0db1d7fed097ec8f26 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 2 Nov 2022 03:19:29 +0100 Subject: [PATCH 121/149] Fix missing use FQCN for WP_Error (#35305) * add use WP_Error phpdoc was not using FQCN for WP_Error, however use is preferred to FQCN anyway Fix: https://github.com/woocommerce/woocommerce/issues/35304 * Revert "add use WP_Error" This reverts commit ff62deb10020689f96722e10c5c86669a1454125. * use FQCN in PHPDoc when using FQCN in code * add changelog --- plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error | 4 ++++ .../src/Admin/Features/OnboardingTasks/TaskLists.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error diff --git a/plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error b/plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error new file mode 100644 index 00000000000..f09f3d26109 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +FQCN for WP_Error in PHPDoc. diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 31cd3a68bf6..03d5e0bf72d 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -262,7 +262,7 @@ class TaskLists { * Add a task list. * * @param array $args Task list properties. - * @return WP_Error|TaskList + * @return \WP_Error|TaskList */ public static function add_list( $args ) { if ( isset( self::$lists[ $args['id'] ] ) ) { @@ -281,7 +281,7 @@ class TaskLists { * * @param string $list_id List ID to add the task to. * @param array $args Task properties. - * @return WP_Error|Task + * @return \WP_Error|Task */ public static function add_task( $list_id, $args ) { if ( ! isset( self::$lists[ $list_id ] ) ) { From 84660ccf6f9f90075b0728155e0f2bb90729180d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 2 Nov 2022 03:21:50 +0100 Subject: [PATCH 122/149] Fix class name for class FirstDownlaodableProduct (#35383) Spelling Fix https://github.com/woocommerce/woocommerce/issues/35307 --- .../changelog/fix-misspelled-first-downlaodable-product | 4 ++++ plugins/woocommerce/src/Internal/Admin/Events.php | 4 ++-- ...DownlaodableProduct.php => FirstDownloadableProduct.php} | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product rename plugins/woocommerce/src/Internal/Admin/Notes/{FirstDownlaodableProduct.php => FirstDownloadableProduct.php} (92%) diff --git a/plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product b/plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product new file mode 100644 index 00000000000..85864007047 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix class name for class FirstDownlaodableProduct diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index 94ed2c4f4d7..1ede546c1e7 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -18,7 +18,7 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks; use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizingProductCatalog; use \Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove; use \Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber; -use \Automattic\WooCommerce\Internal\Admin\Notes\FirstDownlaodableProduct; +use \Automattic\WooCommerce\Internal\Admin\Notes\FirstDownloadableProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstSale; use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; @@ -81,7 +81,7 @@ class Events { CustomizingProductCatalog::class, EditProductsOnTheMove::class, EUVATNumber::class, - FirstDownlaodableProduct::class, + FirstDownloadableProduct::class, FirstProduct::class, InsightFirstSale::class, LaunchChecklist::class, diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownlaodableProduct.php b/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php similarity index 92% rename from plugins/woocommerce/src/Internal/Admin/Notes/FirstDownlaodableProduct.php rename to plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php index 4e7fa864528..e38f22ce385 100644 --- a/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownlaodableProduct.php +++ b/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php @@ -2,7 +2,7 @@ /** * WooCommerce Admin Digital/Downloadable Producdt Handling note provider * - * Adds a note with a link to the downlaodable product handling + * Adds a note with a link to the downloadable product handling */ namespace Automattic\WooCommerce\Internal\Admin\Notes; @@ -13,9 +13,9 @@ use \Automattic\WooCommerce\Admin\Notes\Note; use \Automattic\WooCommerce\Admin\Notes\NoteTraits; /** - * FirstDownlaodableProduct. + * FirstDownloadableProduct. */ -class FirstDownlaodableProduct { +class FirstDownloadableProduct { /** * Note traits. */ From e60be03951abeb7ecbde10cf4624c5691023173a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 2 Nov 2022 15:55:41 +0800 Subject: [PATCH 123/149] Fix invalid `wcadmin_install_plugin_error` event props (#35411) * Fix invalid wcadmin_install_plugin_error props * Add changelog * Update docblock comments to fix lint * Fix lint * Update prop name --- .../fix-103-install-plugin-error-track | 4 ++ .../woocommerce/src/Admin/PluginsHelper.php | 42 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-103-install-plugin-error-track diff --git a/plugins/woocommerce/changelog/fix-103-install-plugin-error-track b/plugins/woocommerce/changelog/fix-103-install-plugin-error-track new file mode 100644 index 00000000000..73059808c1f --- /dev/null +++ b/plugins/woocommerce/changelog/fix-103-install-plugin-error-track @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix invalid wcadmin_install_plugin_error event props diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index 29e22927262..c2c760d2378 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -150,6 +150,7 @@ class PluginsHelper { * Filter the list of plugins to install. * * @param array $plugins A list of the plugins to install. + * @since 6.4.0 */ $plugins = apply_filters( 'woocommerce_admin_plugins_pre_install', $plugins ); @@ -193,12 +194,19 @@ class PluginsHelper { if ( is_wp_error( $api ) ) { $properties = array( /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), - 'api' => $api, - 'slug' => $slug, + 'error_message' => sprintf( __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), $slug ), + 'api_error_message' => $api->get_error_message(), + 'slug' => $slug, ); wc_admin_record_tracks_event( 'install_plugin_error', $properties ); + /** + * Action triggered when a plugin API call failed. + * + * @param string $slug The plugin slug. + * @param \WP_Error $api The API response. + * @since 6.4.0 + */ do_action( 'woocommerce_plugins_install_api_error', $slug, $api ); $errors->add( @@ -221,14 +229,24 @@ class PluginsHelper { if ( is_wp_error( $result ) || is_null( $result ) ) { $properties = array( /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => __( 'The requested plugin `%s` could not be installed.', 'woocommerce' ), - 'slug' => $slug, - 'api' => $api, - 'upgrader' => $upgrader, - 'result' => $result, + 'error_message' => sprintf( __( 'The requested plugin `%s` could not be installed.', 'woocommerce' ), $slug ), + 'slug' => $slug, + 'api_version' => $api->version, + 'api_download_link' => $api->download_link, + 'upgrader_skin_message' => implode( ',', $upgrader->skin->get_upgrade_messages() ), + 'result' => $result, ); wc_admin_record_tracks_event( 'install_plugin_error', $properties ); + /** + * Action triggered when a plugin installation fails. + * + * @param string $slug The plugin slug. + * @param object $api The plugin API object. + * @param \WP_Error|null $result The result of the plugin installation. + * @param \Plugin_Upgrader $upgrader The plugin upgrader. + * @since 6.4.0 + */ do_action( 'woocommerce_plugins_install_error', $slug, $api, $result, $upgrader ); $errors->add( @@ -292,6 +310,7 @@ class PluginsHelper { * Filter the list of plugins to activate. * * @param array $plugins A list of the plugins to activate. + * @since 6.4.0 */ $plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins ); @@ -314,6 +333,13 @@ class PluginsHelper { $result = activate_plugin( $path ); if ( ! is_null( $result ) ) { + /** + * Action triggered when a plugin activation fails. + * + * @param string $slug The plugin slug. + * @param null|\WP_Error $result The result of the plugin activation. + * @since 6.4.0 + */ do_action( 'woocommerce_plugins_activate_error', $slug, $result ); $errors->add( From d77f8fc5cdbaa693c28fb4c830962858a9021244 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Wed, 2 Nov 2022 09:31:52 -0300 Subject: [PATCH 124/149] Product creation experience: shortcut to add variation price (#34948) * Add modal * Add modal style * Open modal * Add validations * Add button * Add changelog * Fix variations price setting * Fix warning for variations without price * Fix styles * Change row actions * Fix actions styles * Fix error text * Remove console.logs * Add save after setting prices * Modify modal copy * Change modal button text * Fix text * Fix text * Fix styles * Fix button * Fix code sniff errors * Fix more code sniff errors * Fix code sniff errors * Fix comments * Fix comment * Fix lint * Fix lint Co-authored-by: Fernando Marichal --- .../changelog/add-33554_variation-price | 4 + .../woocommerce/client/legacy/css/admin.scss | 71 +- .../js/admin/meta-boxes-product-variation.js | 1177 ++++++++++++----- .../legacy/js/admin/woocommerce_admin.js | 775 +++++++---- .../includes/admin/class-wc-admin-assets.php | 20 +- .../class-wc-meta-box-product-data.php | 17 +- .../views/html-product-data-variations.php | 38 + .../meta-boxes/views/html-variation-admin.php | 20 +- .../includes/admin/wc-admin-functions.php | 23 +- 9 files changed, 1483 insertions(+), 662 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-33554_variation-price diff --git a/plugins/woocommerce/changelog/add-33554_variation-price b/plugins/woocommerce/changelog/add-33554_variation-price new file mode 100644 index 00000000000..f6d8cbb756a --- /dev/null +++ b/plugins/woocommerce/changelog/add-33554_variation-price @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add variation price shortcut diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index ecac8fccc44..f50bd3dbf54 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -952,7 +952,43 @@ #variable_product_options #message, #variable_product_options .notice { + display: flex; margin: 10px; + background-color: #FCFAE8; + > p { + width: 85%; + } + .woocommerce-add-variation-price-container { + width: 15%; + display: flex; + justify-content: flex-end; + > button { + align-self: center; + } + } +} + +.woocommerce-set-price-variations { + .woocommerce-usage-modal__wrapper{ + .woocommerce-usage-modal__message { + height: 60px; + flex-wrap: wrap; + display: flex; + > span { + padding-bottom: 16px; + } + } + .woocommerce-usage-modal__actions { + display: flex; + justify-content: flex-end; + margin-top: 20px; + > button{ + margin-left: 16px; + width: 88px; + display: unset; + } + } + } } #variable_product_options { @@ -5090,7 +5126,6 @@ img.help_tip { font-size: 15px; font-weight: 400; margin-right: 0.5em; - visibility: hidden; text-align: center; vertical-align: middle; @@ -5110,6 +5145,9 @@ img.help_tip { color: #777; } } + .edit_variation { + margin-left: 0.5em; + } h3:hover, &.ui-sortable-helper { @@ -5119,6 +5157,14 @@ img.help_tip { } } +.woocommerce_attribute { + h3 { + .sort, a.delete { + visibility: hidden; + } + } +} + .woocommerce_options_panel { min-height: 175px; box-sizing: border-box; @@ -5442,7 +5488,8 @@ img.help_tip { cursor: move; button, - a.delete { + a.delete, + a.edit { float: right; } @@ -5452,7 +5499,17 @@ img.help_tip { line-height: 26px; text-decoration: none; position: relative; - visibility: hidden; + } + + a.edit { + font-weight: normal; + line-height: 26px; + text-decoration: none; + position: relative; + } + + a.remove_variation { + margin: 0 0.5em; } strong { @@ -5484,11 +5541,19 @@ img.help_tip { padding: 0.5em 0.75em 0.5em 1em !important; a.delete, + a.edit, .handlediv, .sort { margin-top: 0.25em; } } + &.woocommerce_variation h3 { + a.delete, + a.edit, + .sort { + margin-top: 0.45em; + } + } h3:hover, &.ui-sortable-helper { diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js index e6949a5fb4b..84eab9d8bd5 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js @@ -1,27 +1,54 @@ /* global wp, woocommerce_admin_meta_boxes_variations, woocommerce_admin, accounting */ -jQuery( function( $ ) { - 'use strict'; +jQuery( function ( $ ) { + 'use strict'; /** * Variations actions */ var wc_meta_boxes_product_variations_actions = { - /** * Initialize variations actions */ - init: function() { + init: function () { $( '#variable_product_options' ) - .on( 'change', 'input.variable_is_downloadable', this.variable_is_downloadable ) - .on( 'change', 'input.variable_is_virtual', this.variable_is_virtual ) - .on( 'change', 'input.variable_manage_stock', this.variable_manage_stock ) + .on( + 'change', + 'input.variable_is_downloadable', + this.variable_is_downloadable + ) + .on( + 'change', + 'input.variable_is_virtual', + this.variable_is_virtual + ) + .on( + 'change', + 'input.variable_manage_stock', + this.variable_manage_stock + ) .on( 'click', 'button.notice-dismiss', this.notice_dismiss ) .on( 'click', 'h3 .sort', this.set_menu_order ) + .on( + 'click', + 'button.add_price_for_variations', + this.open_modal_to_set_variations_price + ) .on( 'reload', this.reload ); - $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock' ).trigger( 'change' ); - $( '#woocommerce-product-data' ).on( 'woocommerce_variations_loaded', this.variations_loaded ); - $( document.body ).on( 'woocommerce_variations_added', this.variation_added ); + $( + 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock' + ).trigger( 'change' ); + $( '#woocommerce-product-data' ).on( + 'woocommerce_variations_loaded', + this.variations_loaded + ); + $( document.body ) + .on( 'woocommerce_variations_added', this.variation_added ) + .on( + 'keyup', + '.wc_input_variations_price', + this.maybe_enable_button_to_add_price_to_variations + ); }, /** @@ -30,7 +57,7 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} qty */ - reload: function() { + reload: function () { wc_meta_boxes_product_variations_ajax.load_variations( 1 ); wc_meta_boxes_product_variations_pagenav.set_paginav( 0 ); }, @@ -38,47 +65,89 @@ jQuery( function( $ ) { /** * Check if variation is downloadable and show/hide elements */ - variable_is_downloadable: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).hide(); + variable_is_downloadable: function () { + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_downloadable' ) + .hide(); if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).show(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_downloadable' ) + .show(); } }, /** * Check if variation is virtual and show/hide elements */ - variable_is_virtual: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).show(); + variable_is_virtual: function () { + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.hide_if_variation_virtual' ) + .show(); if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.hide_if_variation_virtual' ) + .hide(); + } + }, + + /** + * Maybe enable the button to add a price for every variation + */ + maybe_enable_button_to_add_price_to_variations: function () { + var variation_price = parseInt( + $( '.wc_input_variations_price' ).val(), + 10 + ); + if ( isNaN( variation_price ) ) { + $( '.add_variations_price_button' ).prop( 'disabled', true ); + } else { + $( '.add_variations_price_button' ).prop( 'disabled', false ); } }, /** * Check if variation manage stock and show/hide elements */ - variable_manage_stock: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).hide(); - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).show(); + variable_manage_stock: function () { + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_manage_stock' ) + .hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_stock_status' ) + .show(); if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).show(); - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_manage_stock' ) + .show(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_stock_status' ) + .hide(); } // Parent level. if ( $( 'input#_manage_stock:checked' ).length ) { - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_stock_status' ) + .hide(); } }, /** * Notice dismiss */ - notice_dismiss: function() { + notice_dismiss: function () { $( this ).closest( 'div.notice' ).remove(); }, @@ -88,31 +157,43 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} needsUpdate */ - variations_loaded: function( event, needsUpdate ) { + variations_loaded: function ( event, needsUpdate ) { needsUpdate = needsUpdate || false; var wrapper = $( '#woocommerce-product-data' ); if ( ! needsUpdate ) { // Show/hide downloadable, virtual and stock fields - $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock', wrapper ).trigger( 'change' ); + $( + 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock', + wrapper + ).trigger( 'change' ); // Open sale schedule fields when have some sale price date - $( '.woocommerce_variation', wrapper ).each( function( index, el ) { - var $el = $( el ), + $( '.woocommerce_variation', wrapper ).each( function ( + index, + el + ) { + var $el = $( el ), date_from = $( '.sale_price_dates_from', $el ).val(), - date_to = $( '.sale_price_dates_to', $el ).val(); + date_to = $( '.sale_price_dates_to', $el ).val(); if ( '' !== date_from || '' !== date_to ) { $( 'a.sale_schedule', $el ).trigger( 'click' ); } - }); + } ); // Remove variation-needs-update classes - $( '.woocommerce_variations .variation-needs-update', wrapper ).removeClass( 'variation-needs-update' ); + $( + '.woocommerce_variations .variation-needs-update', + wrapper + ).removeClass( 'variation-needs-update' ); // Disable cancel and save buttons - $( 'button.cancel-variation-changes, button.save-variation-changes', wrapper ).attr( 'disabled', 'disabled' ); + $( + 'button.cancel-variation-changes, button.save-variation-changes', + wrapper + ).attr( 'disabled', 'disabled' ); } // Init TipTip @@ -120,48 +201,53 @@ jQuery( function( $ ) { $( '#tiptip_arrow' ).removeAttr( 'style' ); $( '.woocommerce_variations .tips, ' + - '.woocommerce_variations .help_tip, ' + - '.woocommerce_variations .woocommerce-help-tip, ' + - '.toolbar-variations-defaults .woocommerce-help-tip', + '.woocommerce_variations .help_tip, ' + + '.woocommerce_variations .woocommerce-help-tip, ' + + '.toolbar-variations-defaults .woocommerce-help-tip', wrapper - ) - .tipTip({ - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200 - }); + ).tipTip( { + attribute: 'data-tip', + fadeIn: 50, + fadeOut: 50, + delay: 200, + } ); // Datepicker fields - $( '.sale_price_dates_fields', wrapper ).find( 'input' ).datepicker({ - defaultDate: '', - dateFormat: 'yy-mm-dd', - numberOfMonths: 1, - showButtonPanel: true, - onSelect: function() { - var option = $( this ).is( '.sale_price_dates_from' ) ? 'minDate' : 'maxDate', - dates = $( this ).closest( '.sale_price_dates_fields' ).find( 'input' ), - date = $( this ).datepicker( 'getDate' ); + $( '.sale_price_dates_fields', wrapper ) + .find( 'input' ) + .datepicker( { + defaultDate: '', + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true, + onSelect: function () { + var option = $( this ).is( '.sale_price_dates_from' ) + ? 'minDate' + : 'maxDate', + dates = $( this ) + .closest( '.sale_price_dates_fields' ) + .find( 'input' ), + date = $( this ).datepicker( 'getDate' ); - dates.not( this ).datepicker( 'option', option, date ); - $( this ).trigger( 'change' ); - } - }); + dates.not( this ).datepicker( 'option', option, date ); + $( this ).trigger( 'change' ); + }, + } ); // Allow sorting - $( '.woocommerce_variations', wrapper ).sortable({ - items: '.woocommerce_variation', - cursor: 'move', - axis: 'y', - handle: '.sort', - scrollSensitivity: 40, + $( '.woocommerce_variations', wrapper ).sortable( { + items: '.woocommerce_variation', + cursor: 'move', + axis: 'y', + handle: '.sort', + scrollSensitivity: 40, forcePlaceholderSize: true, - helper: 'clone', - opacity: 0.65, - stop: function() { - wc_meta_boxes_product_variations_actions.variation_row_indexes(); - } - }); + helper: 'clone', + opacity: 0.65, + stop: function () { + wc_meta_boxes_product_variations_actions.variation_row_indexes(); + }, + } ); $( document.body ).trigger( 'wc-enhanced-select-init' ); }, @@ -172,32 +258,85 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} qty */ - variation_added: function( event, qty ) { + variation_added: function ( event, qty ) { if ( 1 === qty ) { - wc_meta_boxes_product_variations_actions.variations_loaded( null, true ); + wc_meta_boxes_product_variations_actions.variations_loaded( + null, + true + ); } }, + /** + * Sets a price for every variation + */ + set_variations_price: function () { + var variation_price = $( '.wc_input_variations_price' ).val(); + var product_type = $( 'select#product-type' ).val(); + var input_type = + 'variable-subscription' === product_type + ? 'variable_subscription_sign_up_fee' + : 'variable_regular_price'; + var input = $( `.wc_input_price[name^=${ input_type }]` ); + + // We don't want to override prices already set + input.each( function ( index, el ) { + if ( '0' === $( el ).val() || '' === $( el ).val() ) { + $( el ).val( variation_price ).trigger( 'change' ); + } + } ); + wc_meta_boxes_product_variations_ajax.save_variations(); + }, + + /** + * Opens the modal to set a price for every variation + */ + open_modal_to_set_variations_price: function () { + $( this ).WCBackboneModal( { + template: 'wc-modal-set-price-variations', + } ); + $( '.add_variations_price_button' ).on( + 'click', + wc_meta_boxes_product_variations_actions.set_variations_price + ); + }, + /** * Lets the user manually input menu order to move items around pages */ - set_menu_order: function( event ) { + set_menu_order: function ( event ) { event.preventDefault(); - var $menu_order = $( this ).closest( '.woocommerce_variation' ).find( '.variation_menu_order' ); - var variation_id = $( this ).closest( '.woocommerce_variation' ).find( '.variable_post_id' ).val(); - var value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order, $menu_order.val() ); + var $menu_order = $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variation_menu_order' ); + var variation_id = $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_post_id' ) + .val(); + var value = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order, + $menu_order.val() + ); if ( value != null ) { // Set value, save changes and reload view $menu_order.val( parseInt( value, 10 ) ).trigger( 'change' ); - $( this ).closest( '.woocommerce_variation' ) - .append( '' ); + $( this ) + .closest( '.woocommerce_variation' ) + .append( + '' + ); - $( this ).closest( '.woocommerce_variation' ) - .append( '' ); + $( this ) + .closest( '.woocommerce_variation' ) + .append( + '' + ); wc_meta_boxes_product_variations_ajax.save_variations(); } @@ -206,25 +345,40 @@ jQuery( function( $ ) { /** * Set menu order */ - variation_row_indexes: function() { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + variation_row_indexes: function () { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), - offset = parseInt( ( current_page - 1 ) * woocommerce_admin_meta_boxes_variations.variations_per_page, 10 ); + offset = parseInt( + ( current_page - 1 ) * + woocommerce_admin_meta_boxes_variations.variations_per_page, + 10 + ); - $( '.woocommerce_variations .woocommerce_variation' ).each( function ( index, el ) { - $( '.variation_menu_order', el ) - .val( parseInt( $( el ) - .index( '.woocommerce_variations .woocommerce_variation' ), 10 ) + 1 + offset ) - .trigger( 'change' ); - }); - } + $( '.woocommerce_variations .woocommerce_variation' ).each( + function ( index, el ) { + $( '.variation_menu_order', el ) + .val( + parseInt( + $( el ).index( + '.woocommerce_variations .woocommerce_variation' + ), + 10 + ) + + 1 + + offset + ) + .trigger( 'change' ); + } + ); + }, }; /** * Variations media actions */ var wc_meta_boxes_product_variations_media = { - /** * wp.media frame object * @@ -256,8 +410,12 @@ jQuery( function( $ ) { /** * Initialize media actions */ - init: function() { - $( '#variable_product_options' ).on( 'click', '.upload_image_button', this.add_image ); + init: function () { + $( '#variable_product_options' ).on( + 'click', + '.upload_image_button', + this.add_image + ); $( 'a.add_media' ).on( 'click', this.restore_wp_media_post_id ); }, @@ -266,64 +424,101 @@ jQuery( function( $ ) { * * @param {Object} event */ - add_image: function( event ) { + add_image: function ( event ) { var $button = $( this ), post_id = $button.attr( 'rel' ), $parent = $button.closest( '.upload_image' ); - wc_meta_boxes_product_variations_media.setting_variation_image = $parent; + wc_meta_boxes_product_variations_media.setting_variation_image = $parent; wc_meta_boxes_product_variations_media.setting_variation_image_id = post_id; event.preventDefault(); if ( $button.is( '.remove' ) ) { - - $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( '' ).trigger( 'change' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ) - .attr( 'src', woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).removeClass( 'remove' ); - + $( + '.upload_image_id', + wc_meta_boxes_product_variations_media.setting_variation_image + ) + .val( '' ) + .trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( 'img' ) + .eq( 0 ) + .attr( + 'src', + woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src + ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( '.upload_image_button' ) + .removeClass( 'remove' ); } else { - // If the media frame already exists, reopen it. - if ( wc_meta_boxes_product_variations_media.variable_image_frame ) { - wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader - .param( 'post_id', wc_meta_boxes_product_variations_media.setting_variation_image_id ); + if ( + wc_meta_boxes_product_variations_media.variable_image_frame + ) { + wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader.param( + 'post_id', + wc_meta_boxes_product_variations_media.setting_variation_image_id + ); wc_meta_boxes_product_variations_media.variable_image_frame.open(); return; } else { - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.setting_variation_image_id; + wp.media.model.settings.post.id = + wc_meta_boxes_product_variations_media.setting_variation_image_id; } // Create the media frame. - wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media({ - // Set the title of the modal. - title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, - button: { - text: woocommerce_admin_meta_boxes_variations.i18n_set_image - }, - states: [ - new wp.media.controller.Library({ - title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, - filterable: 'all' - }) - ] - }); + wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media( + { + // Set the title of the modal. + title: + woocommerce_admin_meta_boxes_variations.i18n_choose_image, + button: { + text: + woocommerce_admin_meta_boxes_variations.i18n_set_image, + }, + states: [ + new wp.media.controller.Library( { + title: + woocommerce_admin_meta_boxes_variations.i18n_choose_image, + filterable: 'all', + } ), + ], + } + ); // When an image is selected, run a callback. - wc_meta_boxes_product_variations_media.variable_image_frame.on( 'select', function () { + wc_meta_boxes_product_variations_media.variable_image_frame.on( + 'select', + function () { + var attachment = wc_meta_boxes_product_variations_media.variable_image_frame + .state() + .get( 'selection' ) + .first() + .toJSON(), + url = + attachment.sizes && attachment.sizes.thumbnail + ? attachment.sizes.thumbnail.url + : attachment.url; - var attachment = wc_meta_boxes_product_variations_media.variable_image_frame.state() - .get( 'selection' ).first().toJSON(), - url = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; + $( + '.upload_image_id', + wc_meta_boxes_product_variations_media.setting_variation_image + ) + .val( attachment.id ) + .trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( '.upload_image_button' ) + .addClass( 'remove' ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( 'img' ) + .eq( 0 ) + .attr( 'src', url ); - $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( attachment.id ) - .trigger( 'change' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).addClass( 'remove' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ).attr( 'src', url ); - - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; - }); + wp.media.model.settings.post.id = + wc_meta_boxes_product_variations_media.wp_media_post_id; + } + ); // Finally, open the modal. wc_meta_boxes_product_variations_media.variable_image_frame.open(); @@ -333,41 +528,65 @@ jQuery( function( $ ) { /** * Restore wp.media post ID. */ - restore_wp_media_post_id: function() { - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; - } + restore_wp_media_post_id: function () { + wp.media.model.settings.post.id = + wc_meta_boxes_product_variations_media.wp_media_post_id; + }, }; /** * Product variations metabox ajax methods */ var wc_meta_boxes_product_variations_ajax = { - /** * Initialize variations ajax methods */ - init: function() { + init: function () { $( 'li.variations_tab a' ).on( 'click', this.initial_load ); $( '#variable_product_options' ) - .on( 'click', 'button.save-variation-changes', this.save_variations ) - .on( 'click', 'button.cancel-variation-changes', this.cancel_variations ) + .on( + 'click', + 'button.save-variation-changes', + this.save_variations + ) + .on( + 'click', + 'button.cancel-variation-changes', + this.cancel_variations + ) .on( 'click', '.remove_variation', this.remove_variation ) - .on( 'click','.downloadable_files a.delete', this.input_changed ); + .on( + 'click', + '.downloadable_files a.delete', + this.input_changed + ); $( document.body ) - .on( 'change input', '#variable_product_options .woocommerce_variations :input', this.input_changed ) - .on( 'change', '.variations-defaults select', this.defaults_changed ); + .on( + 'change input', + '#variable_product_options .woocommerce_variations :input', + this.input_changed + ) + .on( + 'change', + '.variations-defaults select', + this.defaults_changed + ); var postForm = $( 'form#post' ); postForm.on( 'submit', this.save_on_submit ); - $( 'input:submit', postForm ).on( 'click keypress', function() { + $( 'input:submit', postForm ).on( 'click keypress', function () { postForm.data( 'callerid', this.id ); - }); + } ); - $( '.wc-metaboxes-wrapper' ).on( 'click', 'a.do_variation_action', this.do_variation_action ); + $( '.wc-metaboxes-wrapper' ).on( + 'click', + 'a.do_variation_action', + this.do_variation_action + ); }, /** @@ -375,11 +594,17 @@ jQuery( function( $ ) { * * @return {Bool} */ - check_for_changes: function() { - var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + check_for_changes: function () { + var need_update = $( '#variable_product_options' ).find( + '.woocommerce_variations .variation-needs-update' + ); if ( 0 < need_update.length ) { - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_edited_variations ) ) { + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_edited_variations + ) + ) { wc_meta_boxes_product_variations_ajax.save_changes(); } else { need_update.removeClass( 'variation-needs-update' ); @@ -393,20 +618,20 @@ jQuery( function( $ ) { /** * Block edit screen */ - block: function() { - $( '#woocommerce-product-data' ).block({ + block: function () { + $( '#woocommerce-product-data' ).block( { message: null, overlayCSS: { background: '#fff', - opacity: 0.6 - } - }); + opacity: 0.6, + }, + } ); }, /** * Unblock edit screen */ - unblock: function() { + unblock: function () { $( '#woocommerce-product-data' ).unblock(); }, @@ -415,8 +640,13 @@ jQuery( function( $ ) { * * @return {Bool} */ - initial_load: function() { - if ( 0 === $( '#variable_product_options' ).find( '.woocommerce_variations .woocommerce_variation' ).length ) { + initial_load: function () { + if ( + 0 === + $( '#variable_product_options' ).find( + '.woocommerce_variations .woocommerce_variation' + ).length + ) { wc_meta_boxes_product_variations_pagenav.go_to_page(); } }, @@ -427,33 +657,43 @@ jQuery( function( $ ) { * @param {Int} page (default: 1) * @param {Int} per_page (default: 10) */ - load_variations: function( page, per_page ) { - page = page || 1; - per_page = per_page || woocommerce_admin_meta_boxes_variations.variations_per_page; + load_variations: function ( page, per_page ) { + page = page || 1; + per_page = + per_page || + woocommerce_admin_meta_boxes_variations.variations_per_page; - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ); wc_meta_boxes_product_variations_ajax.block(); - $.ajax({ + $.ajax( { url: woocommerce_admin_meta_boxes_variations.ajax_url, data: { - action: 'woocommerce_load_variations', - security: woocommerce_admin_meta_boxes_variations.load_variations_nonce, + action: 'woocommerce_load_variations', + security: + woocommerce_admin_meta_boxes_variations.load_variations_nonce, product_id: woocommerce_admin_meta_boxes_variations.post_id, attributes: wrapper.data( 'attributes' ), - page: page, - per_page: per_page + page: page, + per_page: per_page, }, type: 'POST', - success: function( response ) { - wrapper.empty().append( response ).attr( 'data-page', page ); + success: function ( response ) { + wrapper + .empty() + .append( response ) + .attr( 'data-page', page ); - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_loaded' ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_loaded' + ); wc_meta_boxes_product_variations_ajax.unblock(); - } - }); + }, + } ); }, /** @@ -463,13 +703,16 @@ jQuery( function( $ ) { * * @return {Object} */ - get_variations_fields: function( fields ) { + get_variations_fields: function ( fields ) { var data = $( ':input', fields ).serializeJSON(); - $( '.variations-defaults select' ).each( function( index, element ) { + $( '.variations-defaults select' ).each( function ( + index, + element + ) { var select = $( element ); data[ select.attr( 'name' ) ] = select.val(); - }); + } ); return data; }, @@ -479,39 +722,49 @@ jQuery( function( $ ) { * * @param {Function} callback Called once saving is complete */ - save_changes: function( callback ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + save_changes: function ( callback ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), need_update = $( '.variation-needs-update', wrapper ), - data = {}; + data = {}; // Save only with products need update. if ( 0 < need_update.length ) { wc_meta_boxes_product_variations_ajax.block(); - data = wc_meta_boxes_product_variations_ajax.get_variations_fields( need_update ); - data.action = 'woocommerce_save_variations'; - data.security = woocommerce_admin_meta_boxes_variations.save_variations_nonce; - data.product_id = woocommerce_admin_meta_boxes_variations.post_id; - data['product-type'] = $( '#product-type' ).val(); + data = wc_meta_boxes_product_variations_ajax.get_variations_fields( + need_update + ); + data.action = 'woocommerce_save_variations'; + data.security = + woocommerce_admin_meta_boxes_variations.save_variations_nonce; + data.product_id = + woocommerce_admin_meta_boxes_variations.post_id; + data[ 'product-type' ] = $( '#product-type' ).val(); - $.ajax({ + $.ajax( { url: woocommerce_admin_meta_boxes_variations.ajax_url, data: data, type: 'POST', - success: function( response ) { + success: function ( response ) { // Allow change page, delete and add new variations need_update.removeClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).attr( 'disabled', 'disabled' ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).attr( 'disabled', 'disabled' ); - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_saved' ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_saved' + ); if ( typeof callback === 'function' ) { callback( response ); } wc_meta_boxes_product_variations_ajax.unblock(); - } - }); + }, + } ); } }, @@ -520,25 +773,33 @@ jQuery( function( $ ) { * * @return {Bool} */ - save_variations: function() { - $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_button' ); + save_variations: function () { + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_save_variations_button' + ); - wc_meta_boxes_product_variations_ajax.save_changes( function( error ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + wc_meta_boxes_product_variations_ajax.save_changes( function ( + error + ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), current = wrapper.attr( 'data-page' ); - $( '#variable_product_options' ).find( '#woocommerce_errors' ).remove(); + $( '#variable_product_options' ) + .find( '#woocommerce_errors' ) + .remove(); if ( error ) { wrapper.before( error ); } - $( '.variations-defaults select' ).each( function() { + $( '.variations-defaults select' ).each( function () { $( this ).attr( 'data-current', $( this ).val() ); - }); + } ); wc_meta_boxes_product_variations_pagenav.go_to_page( current ); - }); + } ); return false; }, @@ -546,27 +807,41 @@ jQuery( function( $ ) { /** * Save on post form submit */ - save_on_submit: function( e ) { - var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + save_on_submit: function ( e ) { + var need_update = $( '#variable_product_options' ).find( + '.woocommerce_variations .variation-needs-update' + ); if ( 0 < need_update.length ) { e.preventDefault(); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_on_submit' ); - wc_meta_boxes_product_variations_ajax.save_changes( wc_meta_boxes_product_variations_ajax.save_on_submit_done ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_save_variations_on_submit' + ); + wc_meta_boxes_product_variations_ajax.save_changes( + wc_meta_boxes_product_variations_ajax.save_on_submit_done + ); } }, /** * After saved, continue with form submission */ - save_on_submit_done: function() { + save_on_submit_done: function () { var postForm = $( 'form#post' ), callerid = postForm.data( 'callerid' ); if ( 'publish' === callerid ) { - postForm.append('').trigger( 'submit' ); + postForm + .append( + '' + ) + .trigger( 'submit' ); } else { - postForm.append('').trigger( 'submit' ); + postForm + .append( + '' + ) + .trigger( 'submit' ); } }, @@ -575,14 +850,20 @@ jQuery( function( $ ) { * * @return {Bool} */ - cancel_variations: function() { - var current = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-page' ), 10 ); + cancel_variations: function () { + var current = parseInt( + $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .attr( 'data-page' ), + 10 + ); - $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ) + $( '#variable_product_options' ) + .find( '.woocommerce_variations .variation-needs-update' ) .removeClass( 'variation-needs-update' ); - $( '.variations-defaults select' ).each( function() { + $( '.variations-defaults select' ).each( function () { $( this ).val( $( this ).attr( 'data-current' ) ); - }); + } ); wc_meta_boxes_product_variations_pagenav.go_to_page( current ); @@ -594,26 +875,38 @@ jQuery( function( $ ) { * * @return {Bool} */ - add_variation: function() { + add_variation: function () { wc_meta_boxes_product_variations_ajax.block(); var data = { action: 'woocommerce_add_variation', post_id: woocommerce_admin_meta_boxes_variations.post_id, loop: $( '.woocommerce_variation' ).length, - security: woocommerce_admin_meta_boxes_variations.add_variation_nonce + security: + woocommerce_admin_meta_boxes_variations.add_variation_nonce, }; - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { - var variation = $( response ); - variation.addClass( 'variation-needs-update' ); + $.post( + woocommerce_admin_meta_boxes_variations.ajax_url, + data, + function ( response ) { + var variation = $( response ); + variation.addClass( 'variation-needs-update' ); - $( '.woocommerce-notice-invalid-variation' ).remove(); - $( '#variable_product_options' ).find( '.woocommerce_variations' ).prepend( variation ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', 1 ); - wc_meta_boxes_product_variations_ajax.unblock(); - }); + $( '.woocommerce-notice-invalid-variation' ).remove(); + $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .prepend( variation ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).prop( 'disabled', false ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_added', + 1 + ); + wc_meta_boxes_product_variations_ajax.unblock(); + } + ); return false; }, @@ -623,14 +916,18 @@ jQuery( function( $ ) { * * @return {Bool} */ - remove_variation: function() { + remove_variation: function () { wc_meta_boxes_product_variations_ajax.check_for_changes(); - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_remove_variation ) ) { - var variation = $( this ).attr( 'rel' ), + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_remove_variation + ) + ) { + var variation = $( this ).attr( 'rel' ), variation_ids = [], - data = { - action: 'woocommerce_remove_variations' + data = { + action: 'woocommerce_remove_variations', }; wc_meta_boxes_product_variations_ajax.block(); @@ -639,27 +936,52 @@ jQuery( function( $ ) { variation_ids.push( variation ); data.variation_ids = variation_ids; - data.security = woocommerce_admin_meta_boxes_variations.delete_variations_nonce; + data.security = + woocommerce_admin_meta_boxes_variations.delete_variations_nonce; - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function() { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), - total_pages = Math.ceil( ( - parseInt( wrapper.attr( 'data-total' ), 10 ) - 1 - ) / woocommerce_admin_meta_boxes_variations.variations_per_page ), - page = 1; + $.post( + woocommerce_admin_meta_boxes_variations.ajax_url, + data, + function () { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + current_page = parseInt( + wrapper.attr( 'data-page' ), + 10 + ), + total_pages = Math.ceil( + ( parseInt( + wrapper.attr( 'data-total' ), + 10 + ) - + 1 ) / + woocommerce_admin_meta_boxes_variations.variations_per_page + ), + page = 1; - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_removed' ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_removed' + ); - if ( current_page === total_pages || current_page <= total_pages ) { - page = current_page; - } else if ( current_page > total_pages && 0 !== total_pages ) { - page = total_pages; + if ( + current_page === total_pages || + current_page <= total_pages + ) { + page = current_page; + } else if ( + current_page > total_pages && + 0 !== total_pages + ) { + page = total_pages; + } + + wc_meta_boxes_product_variations_pagenav.go_to_page( + page, + -1 + ); } - - wc_meta_boxes_product_variations_pagenav.go_to_page( page, -1 ); - }); - + ); } else { wc_meta_boxes_product_variations_ajax.unblock(); } @@ -673,36 +995,61 @@ jQuery( function( $ ) { * * @return {Bool} */ - link_all_variations: function() { + link_all_variations: function () { wc_meta_boxes_product_variations_ajax.check_for_changes(); - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_link_all_variations ) ) { + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_link_all_variations + ) + ) { wc_meta_boxes_product_variations_ajax.block(); var data = { action: 'woocommerce_link_all_variations', post_id: woocommerce_admin_meta_boxes_variations.post_id, - security: woocommerce_admin_meta_boxes_variations.link_variation_nonce + security: + woocommerce_admin_meta_boxes_variations.link_variation_nonce, }; - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { - var count = parseInt( response, 10 ); + $.post( + woocommerce_admin_meta_boxes_variations.ajax_url, + data, + function ( response ) { + var count = parseInt( response, 10 ); - if ( 1 === count ) { - window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variation_added ); - } else if ( 0 === count || count > 1 ) { - window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variations_added ); - } else { - window.alert( woocommerce_admin_meta_boxes_variations.i18n_no_variations_added ); - } + if ( 1 === count ) { + window.alert( + count + + ' ' + + woocommerce_admin_meta_boxes_variations.i18n_variation_added + ); + } else if ( 0 === count || count > 1 ) { + window.alert( + count + + ' ' + + woocommerce_admin_meta_boxes_variations.i18n_variations_added + ); + } else { + window.alert( + woocommerce_admin_meta_boxes_variations.i18n_no_variations_added + ); + } - if ( count > 0 ) { - wc_meta_boxes_product_variations_pagenav.go_to_page( 1, count ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', count ); - } else { - wc_meta_boxes_product_variations_ajax.unblock(); + if ( count > 0 ) { + wc_meta_boxes_product_variations_pagenav.go_to_page( + 1, + count + ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_added', + count + ); + } else { + wc_meta_boxes_product_variations_ajax.unblock(); + } } - }); + ); } return false; @@ -711,87 +1058,119 @@ jQuery( function( $ ) { /** * Add new class when have changes in some input */ - input_changed: function( event ) { + input_changed: function ( event ) { $( this ) .closest( '.woocommerce_variation' ) .addClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).prop( 'disabled', false ); // Do not trigger 'woocommerce_variations_input_changed' for 'input' events for backwards compat. if ( 'input' === event.type && $( this ).is( ':text' ) ) { return; } - $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_input_changed' + ); }, /** * Added new .variation-needs-update class when defaults is changed */ - defaults_changed: function() { + defaults_changed: function () { $( this ) .closest( '#variable_product_options' ) .find( '.woocommerce_variation:first' ) .addClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).prop( 'disabled', false ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_defaults_changed' ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_defaults_changed' + ); }, /** * Actions */ - do_variation_action: function() { + do_variation_action: function () { var do_variation_action = $( 'select.variation_actions' ).val(), - data = {}, - changes = 0, + data = {}, + changes = 0, value; switch ( do_variation_action ) { - case 'add_variation' : + case 'add_variation': wc_meta_boxes_product_variations_ajax.add_variation(); return; - case 'link_all_variations' : + case 'link_all_variations': wc_meta_boxes_product_variations_ajax.link_all_variations(); return; - case 'delete_all' : - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations ) ) { - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_last_warning ) ) { + case 'delete_all': + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations + ) + ) { + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_last_warning + ) + ) { data.allowed = true; - changes = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ) - .attr( 'data-total' ), 10 ) * -1; + changes = + parseInt( + $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .attr( 'data-total' ), + 10 + ) * -1; } } break; - case 'variable_regular_price_increase' : - case 'variable_regular_price_decrease' : - case 'variable_sale_price_increase' : - case 'variable_sale_price_decrease' : - value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent ); + case 'variable_regular_price_increase': + case 'variable_regular_price_decrease': + case 'variable_sale_price_increase': + case 'variable_sale_price_decrease': + value = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent + ); if ( value != null ) { if ( value.indexOf( '%' ) >= 0 ) { - data.value = accounting.unformat( value.replace( /\%/, '' ), woocommerce_admin.mon_decimal_point ) + '%'; + data.value = + accounting.unformat( + value.replace( /\%/, '' ), + woocommerce_admin.mon_decimal_point + ) + '%'; } else { - data.value = accounting.unformat( value, woocommerce_admin.mon_decimal_point ); + data.value = accounting.unformat( + value, + woocommerce_admin.mon_decimal_point + ); } } else { return; } break; - case 'variable_regular_price' : - case 'variable_sale_price' : - case 'variable_stock' : - case 'variable_low_stock_amount' : - case 'variable_weight' : - case 'variable_length' : - case 'variable_width' : - case 'variable_height' : - case 'variable_download_limit' : - case 'variable_download_expiry' : - value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value ); + case 'variable_regular_price': + case 'variable_sale_price': + case 'variable_stock': + case 'variable_low_stock_amount': + case 'variable_weight': + case 'variable_length': + case 'variable_width': + case 'variable_height': + case 'variable_download_limit': + case 'variable_download_expiry': + value = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_enter_a_value + ); if ( value != null ) { data.value = value; @@ -799,9 +1178,13 @@ jQuery( function( $ ) { return; } break; - case 'variable_sale_schedule' : - data.date_from = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start ); - data.date_to = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end ); + case 'variable_sale_schedule': + data.date_from = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start + ); + data.date_to = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end + ); if ( null === data.date_from ) { data.date_from = false; @@ -815,9 +1198,14 @@ jQuery( function( $ ) { return; } break; - default : - $( 'select.variation_actions' ).trigger( do_variation_action ); - data = $( 'select.variation_actions' ).triggerHandler( do_variation_action + '_ajax_data', data ); + default: + $( 'select.variation_actions' ).trigger( + do_variation_action + ); + data = $( 'select.variation_actions' ).triggerHandler( + do_variation_action + '_ajax_data', + data + ); if ( null === data ) { return; @@ -826,47 +1214,67 @@ jQuery( function( $ ) { } if ( 'delete_all' === do_variation_action && data.allowed ) { - $( '#variable_product_options' ).find( '.variation-needs-update' ).removeClass( 'variation-needs-update' ); + $( '#variable_product_options' ) + .find( '.variation-needs-update' ) + .removeClass( 'variation-needs-update' ); } else { wc_meta_boxes_product_variations_ajax.check_for_changes(); } wc_meta_boxes_product_variations_ajax.block(); - $.ajax({ + $.ajax( { url: woocommerce_admin_meta_boxes_variations.ajax_url, data: { - action: 'woocommerce_bulk_edit_variations', - security: woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, - product_id: woocommerce_admin_meta_boxes_variations.post_id, + action: 'woocommerce_bulk_edit_variations', + security: + woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, + product_id: woocommerce_admin_meta_boxes_variations.post_id, product_type: $( '#product-type' ).val(), - bulk_action: do_variation_action, - data: data + bulk_action: do_variation_action, + data: data, }, type: 'POST', - success: function() { - wc_meta_boxes_product_variations_pagenav.go_to_page( 1, changes ); - } - }); - } + success: function () { + wc_meta_boxes_product_variations_pagenav.go_to_page( + 1, + changes + ); + }, + } ); + }, }; /** * Product variations pagenav */ var wc_meta_boxes_product_variations_pagenav = { - /** * Initialize products variations meta box */ - init: function() { + init: function () { $( document.body ) - .on( 'woocommerce_variations_added', this.update_single_quantity ) - .on( 'change', '.variations-pagenav .page-selector', this.page_selector ) - .on( 'click', '.variations-pagenav .first-page', this.first_page ) + .on( + 'woocommerce_variations_added', + this.update_single_quantity + ) + .on( + 'change', + '.variations-pagenav .page-selector', + this.page_selector + ) + .on( + 'click', + '.variations-pagenav .first-page', + this.first_page + ) .on( 'click', '.variations-pagenav .prev-page', this.prev_page ) .on( 'click', '.variations-pagenav .next-page', this.next_page ) - .on( 'click', '.variations-pagenav .last-page', this.last_page ); + .on( + 'click', + '.variations-pagenav .last-page', + this.last_page + ); }, /** @@ -876,18 +1284,30 @@ jQuery( function( $ ) { * * @return {Int} */ - update_variations_count: function( qty ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, + update_variations_count: function ( qty ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, displaying_num = $( '.variations-pagenav .displaying-num' ); // Set the new total of variations wrapper.attr( 'data-total', total ); if ( 1 === total ) { - displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( '%qty%', total ) ); + displaying_num.text( + woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( + '%qty%', + total + ) + ); } else { - displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( '%qty%', total ) ); + displaying_num.text( + woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( + '%qty%', + total + ) + ); } return total; @@ -899,11 +1319,13 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} qty */ - update_single_quantity: function( event, qty ) { + update_single_quantity: function ( event, qty ) { if ( 1 === qty ) { var page_nav = $( '.variations-pagenav' ); - wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ); + wc_meta_boxes_product_variations_pagenav.update_variations_count( + qty + ); if ( page_nav.is( ':hidden' ) ) { $( 'option, optgroup', '.variation_actions' ).show(); @@ -920,15 +1342,22 @@ jQuery( function( $ ) { * * @param {Int} qty */ - set_paginav: function( qty ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ), - toolbar = $( '#variable_product_options' ).find( '.toolbar' ), + set_paginav: function ( qty ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( + qty + ), + toolbar = $( '#variable_product_options' ).find( '.toolbar' ), variation_action = $( '.variation_actions' ), - page_nav = $( '.variations-pagenav' ), + page_nav = $( '.variations-pagenav' ), displaying_links = $( '.pagination-links', page_nav ), - total_pages = Math.ceil( new_qty / woocommerce_admin_meta_boxes_variations.variations_per_page ), - options = ''; + total_pages = Math.ceil( + new_qty / + woocommerce_admin_meta_boxes_variations.variations_per_page + ), + options = ''; // Set the new total of pages wrapper.attr( 'data-total_pages', total_pages ); @@ -949,7 +1378,6 @@ jQuery( function( $ ) { $( 'option, optgroup', variation_action ).hide(); $( '.variation_actions' ).val( 'add_variation' ); $( 'option[data-global="true"]', variation_action ).show(); - } else { toolbar.show(); page_nav.show(); @@ -970,18 +1398,18 @@ jQuery( function( $ ) { * * @return {Bool} */ - check_is_enabled: function( current ) { + check_is_enabled: function ( current ) { return ! $( current ).hasClass( 'disabled' ); }, /** * Change "disabled" class on pagenav */ - change_classes: function( selected, total ) { + change_classes: function ( selected, total ) { var first_page = $( '.variations-pagenav .first-page' ), - prev_page = $( '.variations-pagenav .prev-page' ), - next_page = $( '.variations-pagenav .next-page' ), - last_page = $( '.variations-pagenav .last-page' ); + prev_page = $( '.variations-pagenav .prev-page' ), + next_page = $( '.variations-pagenav .next-page' ), + last_page = $( '.variations-pagenav .last-page' ); if ( 1 === selected ) { first_page.addClass( 'disabled' ); @@ -1003,8 +1431,11 @@ jQuery( function( $ ) { /** * Set page */ - set_page: function( page ) { - $( '.variations-pagenav .page-selector' ).val( page ).first().trigger( 'change' ); + set_page: function ( page ) { + $( '.variations-pagenav .page-selector' ) + .val( page ) + .first() + .trigger( 'change' ); }, /** @@ -1013,9 +1444,9 @@ jQuery( function( $ ) { * @param {Int} page * @param {Int} qty */ - go_to_page: function( page, qty ) { + go_to_page: function ( page, qty ) { page = page || 1; - qty = qty || 0; + qty = qty || 0; wc_meta_boxes_product_variations_pagenav.set_paginav( qty ); wc_meta_boxes_product_variations_pagenav.set_page( page ); @@ -1024,14 +1455,19 @@ jQuery( function( $ ) { /** * Paginav pagination selector */ - page_selector: function() { + page_selector: function () { var selected = parseInt( $( this ).val(), 10 ), - wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ); $( '.variations-pagenav .page-selector' ).val( selected ); wc_meta_boxes_product_variations_ajax.check_for_changes(); - wc_meta_boxes_product_variations_pagenav.change_classes( selected, parseInt( wrapper.attr( 'data-total_pages' ), 10 ) ); + wc_meta_boxes_product_variations_pagenav.change_classes( + selected, + parseInt( wrapper.attr( 'data-total_pages' ), 10 ) + ); wc_meta_boxes_product_variations_ajax.load_variations( selected ); }, @@ -1040,8 +1476,12 @@ jQuery( function( $ ) { * * @return {Bool} */ - first_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + first_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { wc_meta_boxes_product_variations_pagenav.set_page( 1 ); } @@ -1053,11 +1493,17 @@ jQuery( function( $ ) { * * @return {Bool} */ - prev_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + prev_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), prev_page = parseInt( wrapper.attr( 'data-page' ), 10 ) - 1, - new_page = ( 0 < prev_page ) ? prev_page : 1; + new_page = 0 < prev_page ? prev_page : 1; wc_meta_boxes_product_variations_pagenav.set_page( new_page ); } @@ -1070,12 +1516,22 @@ jQuery( function( $ ) { * * @return {Bool} */ - next_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - total_pages = parseInt( wrapper.attr( 'data-total_pages' ), 10 ), - next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, - new_page = ( total_pages >= next_page ) ? next_page : total_pages; + next_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + total_pages = parseInt( + wrapper.attr( 'data-total_pages' ), + 10 + ), + next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, + new_page = + total_pages >= next_page ? next_page : total_pages; wc_meta_boxes_product_variations_pagenav.set_page( new_page ); } @@ -1088,20 +1544,25 @@ jQuery( function( $ ) { * * @return {Bool} */ - last_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var last_page = $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-total_pages' ); + last_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { + var last_page = $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .attr( 'data-total_pages' ); wc_meta_boxes_product_variations_pagenav.set_page( last_page ); } return false; - } + }, }; wc_meta_boxes_product_variations_actions.init(); wc_meta_boxes_product_variations_media.init(); wc_meta_boxes_product_variations_ajax.init(); wc_meta_boxes_product_variations_pagenav.init(); - -}); +} ); diff --git a/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js index 7db81de1724..4b34a0d1302 100644 --- a/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js +++ b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js @@ -1,32 +1,32 @@ /* global woocommerce_admin */ -( function( $, woocommerce_admin ) { - $( function() { +( function ( $, woocommerce_admin ) { + $( function () { if ( 'undefined' === typeof woocommerce_admin ) { return; } // Add buttons to product screen. var $product_screen = $( '.edit-php.post-type-product' ), - $title_action = $product_screen.find( '.page-title-action:first' ), - $blankslate = $product_screen.find( '.woocommerce-BlankState' ); + $title_action = $product_screen.find( '.page-title-action:first' ), + $blankslate = $product_screen.find( '.woocommerce-BlankState' ); if ( 0 === $blankslate.length ) { if ( woocommerce_admin.urls.export_products ) { $title_action.after( '' + - woocommerce_admin.strings.export_products + - '' + woocommerce_admin.urls.export_products + + '" class="page-title-action">' + + woocommerce_admin.strings.export_products + + '' ); } if ( woocommerce_admin.urls.import_products ) { $title_action.after( '' + - woocommerce_admin.strings.import_products + - '' + woocommerce_admin.urls.import_products + + '" class="page-title-action">' + + woocommerce_admin.strings.import_products + + '' ); } } else { @@ -34,60 +34,103 @@ } // Progress indicators when showing steps. - $( '.woocommerce-progress-form-wrapper .button-next' ).on( 'click', function() { - $('.wc-progress-form-content').block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - return true; - } ); + $( '.woocommerce-progress-form-wrapper .button-next' ).on( + 'click', + function () { + $( '.wc-progress-form-content' ).block( { + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6, + }, + } ); + return true; + } + ); // Field validation error tips $( document.body ) - - .on( 'wc_add_error_tip', function( e, element, error_type ) { + .on( 'wc_add_error_tip', function ( e, element, error_type ) { var offset = element.position(); if ( element.parent().find( '.wc_error_tip' ).length === 0 ) { - element.after( '
' + woocommerce_admin[error_type] + '
' ); - element.parent().find( '.wc_error_tip' ) - .css( 'left', offset.left + element.width() - ( element.width() / 2 ) - ( $( '.wc_error_tip' ).width() / 2 ) ) + element.after( + '
' + + woocommerce_admin[ error_type ] + + '
' + ); + element + .parent() + .find( '.wc_error_tip' ) + .css( + 'left', + offset.left + + element.width() - + element.width() / 2 - + $( '.wc_error_tip' ).width() / 2 + ) .css( 'top', offset.top + element.height() ) .fadeIn( '100' ); } - }) + } ) - .on( 'wc_remove_error_tip', function( e, element, error_type ) { - element.parent().find( '.wc_error_tip.' + error_type ).fadeOut( '100', function() { $( this ).remove(); } ); - }) + .on( 'wc_remove_error_tip', function ( e, element, error_type ) { + element + .parent() + .find( '.wc_error_tip.' + error_type ) + .fadeOut( '100', function () { + $( this ).remove(); + } ); + } ) - .on( 'click', function() { - $( '.wc_error_tip' ).fadeOut( '100', function() { $( this ).remove(); } ); - }) + .on( 'click', function () { + $( '.wc_error_tip' ).fadeOut( '100', function () { + $( this ).remove(); + } ); + } ) - .on( 'blur', '.wc_input_decimal[type=text], .wc_input_price[type=text], .wc_input_country_iso[type=text]', function() { - $( '.wc_error_tip' ).fadeOut( '100', function() { $( this ).remove(); } ); - }) + .on( + 'blur', + '.wc_input_decimal[type=text], .wc_input_price[type=text], .wc_input_country_iso[type=text]', + function () { + $( '.wc_error_tip' ).fadeOut( '100', function () { + $( this ).remove(); + } ); + } + ) .on( 'change', - '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc-order-totals #refund_amount[type=text]', - function() { - var regex, decimalRegex, + '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc-order-totals #refund_amount[type=text], ' + + '.wc_input_variations_price[type=text]', + function () { + var regex, + decimalRegex, decimailPoint = woocommerce_admin.decimal_point; - if ( $( this ).is( '.wc_input_price' ) || $( this ).is( '#refund_amount' ) ) { + if ( + $( this ).is( '.wc_input_price' ) || + $( this ).is( '.wc_input_variations_price' ) || + $( this ).is( '#refund_amount' ) + ) { decimailPoint = woocommerce_admin.mon_decimal_point; } - regex = new RegExp( '[^\-0-9\%\\' + decimailPoint + ']+', 'gi' ); - decimalRegex = new RegExp( '\\' + decimailPoint + '+', 'gi' ); + regex = new RegExp( + '[^-0-9%\\' + decimailPoint + ']+', + 'gi' + ); + decimalRegex = new RegExp( + '\\' + decimailPoint + '+', + 'gi' + ); - var value = $( this ).val(); - var newvalue = value.replace( regex, '' ).replace( decimalRegex, decimailPoint ); + var value = $( this ).val(); + var newvalue = value + .replace( regex, '' ) + .replace( decimalRegex, decimailPoint ); if ( value !== newvalue ) { $( this ).val( newvalue ); @@ -98,120 +141,197 @@ .on( 'keyup', // eslint-disable-next-line max-len - '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc_input_country_iso[type=text], .wc-order-totals #refund_amount[type=text]', - function() { + '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc_input_country_iso[type=text], .wc-order-totals #refund_amount[type=text], .wc_input_variations_price[type=text]', + function () { var regex, error, decimalRegex; var checkDecimalNumbers = false; - - if ( $( this ).is( '.wc_input_price' ) || $( this ).is( '#refund_amount' ) ) { + if ( + $( this ).is( '.wc_input_price' ) || + $( this ).is( '.wc_input_variations_price' ) || + $( this ).is( '#refund_amount' ) + ) { checkDecimalNumbers = true; - regex = new RegExp( '[^\-0-9\%\\' + woocommerce_admin.mon_decimal_point + ']+', 'gi' ); - decimalRegex = new RegExp( '[^\\' + woocommerce_admin.mon_decimal_point + ']', 'gi' ); + regex = new RegExp( + '[^-0-9%\\' + + woocommerce_admin.mon_decimal_point + + ']+', + 'gi' + ); + decimalRegex = new RegExp( + '[^\\' + woocommerce_admin.mon_decimal_point + ']', + 'gi' + ); error = 'i18n_mon_decimal_error'; } else if ( $( this ).is( '.wc_input_country_iso' ) ) { regex = new RegExp( '([^A-Z])+|(.){3,}', 'im' ); error = 'i18n_country_iso_error'; } else { checkDecimalNumbers = true; - regex = new RegExp( '[^\-0-9\%\\' + woocommerce_admin.decimal_point + ']+', 'gi' ); - decimalRegex = new RegExp( '[^\\' + woocommerce_admin.decimal_point + ']', 'gi' ); + regex = new RegExp( + '[^-0-9%\\' + + woocommerce_admin.decimal_point + + ']+', + 'gi' + ); + decimalRegex = new RegExp( + '[^\\' + woocommerce_admin.decimal_point + ']', + 'gi' + ); error = 'i18n_decimal_error'; } - var value = $( this ).val(); + var value = $( this ).val(); var newvalue = value.replace( regex, '' ); // Check if newvalue have more than one decimal point. - if ( checkDecimalNumbers && 1 < newvalue.replace( decimalRegex, '' ).length ) { + if ( + checkDecimalNumbers && + 1 < newvalue.replace( decimalRegex, '' ).length + ) { newvalue = newvalue.replace( decimalRegex, '' ); } if ( value !== newvalue ) { - $( document.body ).triggerHandler( 'wc_add_error_tip', [ $( this ), error ] ); + $( document.body ).triggerHandler( 'wc_add_error_tip', [ + $( this ), + error, + ] ); } else { - $( document.body ).triggerHandler( 'wc_remove_error_tip', [ $( this ), error ] ); + $( + document.body + ).triggerHandler( 'wc_remove_error_tip', [ + $( this ), + error, + ] ); } } ) - .on( 'change', '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', function() { - var sale_price_field = $( this ), regular_price_field; + .on( + 'change', + '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', + function () { + var sale_price_field = $( this ), + regular_price_field; - if ( sale_price_field.attr( 'name' ).indexOf( 'variable' ) !== -1 ) { - regular_price_field = sale_price_field - .parents( '.variable_pricing' ) - .find( '.wc_input_price[name^=variable_regular_price]' ); - } else { - regular_price_field = $( '#_regular_price' ); + if ( + sale_price_field + .attr( 'name' ) + .indexOf( 'variable' ) !== -1 + ) { + regular_price_field = sale_price_field + .parents( '.variable_pricing' ) + .find( + '.wc_input_price[name^=variable_regular_price]' + ); + } else { + regular_price_field = $( '#_regular_price' ); + } + + var sale_price = parseFloat( + window.accounting.unformat( + sale_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + var regular_price = parseFloat( + window.accounting.unformat( + regular_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + + if ( sale_price >= regular_price ) { + $( this ).val( '' ); + } } + ) - var sale_price = parseFloat( - window.accounting.unformat( sale_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); - var regular_price = parseFloat( - window.accounting.unformat( regular_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); + .on( + 'keyup', + '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', + function () { + var sale_price_field = $( this ), + regular_price_field; - if ( sale_price >= regular_price ) { - $( this ).val( '' ); + if ( + sale_price_field + .attr( 'name' ) + .indexOf( 'variable' ) !== -1 + ) { + regular_price_field = sale_price_field + .parents( '.variable_pricing' ) + .find( + '.wc_input_price[name^=variable_regular_price]' + ); + } else { + regular_price_field = $( '#_regular_price' ); + } + + var sale_price = parseFloat( + window.accounting.unformat( + sale_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + var regular_price = parseFloat( + window.accounting.unformat( + regular_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + + if ( sale_price >= regular_price ) { + $( document.body ).triggerHandler( 'wc_add_error_tip', [ + $( this ), + 'i18n_sale_less_than_regular_error', + ] ); + } else { + $( + document.body + ).triggerHandler( 'wc_remove_error_tip', [ + $( this ), + 'i18n_sale_less_than_regular_error', + ] ); + } } - }) - - .on( 'keyup', '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', function() { - var sale_price_field = $( this ), regular_price_field; - - if ( sale_price_field.attr( 'name' ).indexOf( 'variable' ) !== -1 ) { - regular_price_field = sale_price_field - .parents( '.variable_pricing' ) - .find( '.wc_input_price[name^=variable_regular_price]' ); - } else { - regular_price_field = $( '#_regular_price' ); - } - - var sale_price = parseFloat( - window.accounting.unformat( sale_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); - var regular_price = parseFloat( - window.accounting.unformat( regular_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); - - if ( sale_price >= regular_price ) { - $( document.body ).triggerHandler( 'wc_add_error_tip', [ $(this), 'i18n_sale_less_than_regular_error' ] ); - } else { - $( document.body ).triggerHandler( 'wc_remove_error_tip', [ $(this), 'i18n_sale_less_than_regular_error' ] ); - } - }) - - .on( 'init_tooltips', function() { + ) + .on( 'init_tooltips', function () { $( '.tips, .help_tip, .woocommerce-help-tip' ).tipTip( { - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200, - 'keepAlive': true + attribute: 'data-tip', + fadeIn: 50, + fadeOut: 50, + delay: 200, + keepAlive: true, } ); $( '.column-wc_actions .wc-action-button' ).tipTip( { - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200 + fadeIn: 50, + fadeOut: 50, + delay: 200, } ); // Add tiptip to parent element for widefat tables - $( '.parent-tips' ).each( function() { - $( this ).closest( 'a, th' ).attr( 'data-tip', $( this ).data( 'tip' ) ).tipTip( { - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200, - 'keepAlive': true - } ).css( 'cursor', 'help' ); - }); - }) + $( '.parent-tips' ).each( function () { + $( this ) + .closest( 'a, th' ) + .attr( 'data-tip', $( this ).data( 'tip' ) ) + .tipTip( { + attribute: 'data-tip', + fadeIn: 50, + fadeOut: 50, + delay: 200, + keepAlive: true, + } ) + .css( 'cursor', 'help' ); + } ); + } ) - .on( 'click', '.wc-confirm-delete', function( event ) { - if ( ! window.confirm( woocommerce_admin.i18n_confirm_delete ) ) { + .on( 'click', '.wc-confirm-delete', function ( event ) { + if ( + ! window.confirm( woocommerce_admin.i18n_confirm_delete ) + ) { event.stopPropagation(); } } ); @@ -220,7 +340,7 @@ $( document.body ).trigger( 'init_tooltips' ); // wc_input_table tables - $( '.wc_input_table.sortable tbody' ).sortable({ + $( '.wc_input_table.sortable tbody' ).sortable( { items: 'tr', cursor: 'move', axis: 'y', @@ -229,205 +349,290 @@ helper: 'clone', opacity: 0.65, placeholder: 'wc-metabox-sortable-placeholder', - start: function( event, ui ) { + start: function ( event, ui ) { ui.item.css( 'background-color', '#f6f6f6' ); }, - stop: function( event, ui ) { + stop: function ( event, ui ) { ui.item.removeAttr( 'style' ); - } - }); + }, + } ); // Focus on inputs within the table if clicked instead of trying to sort. - $( '.wc_input_table.sortable tbody input' ).on( 'click', function() { + $( '.wc_input_table.sortable tbody input' ).on( 'click', function () { $( this ).trigger( 'focus' ); } ); - $( '.wc_input_table .remove_rows' ).on( 'click', function() { + $( '.wc_input_table .remove_rows' ).on( 'click', function () { var $tbody = $( this ).closest( '.wc_input_table' ).find( 'tbody' ); if ( $tbody.find( 'tr.current' ).length > 0 ) { var $current = $tbody.find( 'tr.current' ); - $current.each( function() { + $current.each( function () { $( this ).remove(); - }); + } ); } return false; - }); + } ); var controlled = false; - var shifted = false; - var hasFocus = false; + var shifted = false; + var hasFocus = false; - $( document.body ).on( 'keyup keydown', function( e ) { - shifted = e.shiftKey; + $( document.body ).on( 'keyup keydown', function ( e ) { + shifted = e.shiftKey; controlled = e.ctrlKey || e.metaKey; - }); + } ); - $( '.wc_input_table' ).on( 'focus click', 'input', function( e ) { - var $this_table = $( this ).closest( 'table, tbody' ); - var $this_row = $( this ).closest( 'tr' ); + $( '.wc_input_table' ) + .on( 'focus click', 'input', function ( e ) { + var $this_table = $( this ).closest( 'table, tbody' ); + var $this_row = $( this ).closest( 'tr' ); - if ( ( e.type === 'focus' && hasFocus !== $this_row.index() ) || ( e.type === 'click' && $( this ).is( ':focus' ) ) ) { - hasFocus = $this_row.index(); + if ( + ( e.type === 'focus' && hasFocus !== $this_row.index() ) || + ( e.type === 'click' && $( this ).is( ':focus' ) ) + ) { + hasFocus = $this_row.index(); - if ( ! shifted && ! controlled ) { - $( 'tr', $this_table ).removeClass( 'current' ).removeClass( 'last_selected' ); - $this_row.addClass( 'current' ).addClass( 'last_selected' ); - } else if ( shifted ) { - $( 'tr', $this_table ).removeClass( 'current' ); - $this_row.addClass( 'selected_now' ).addClass( 'current' ); + if ( ! shifted && ! controlled ) { + $( 'tr', $this_table ) + .removeClass( 'current' ) + .removeClass( 'last_selected' ); + $this_row + .addClass( 'current' ) + .addClass( 'last_selected' ); + } else if ( shifted ) { + $( 'tr', $this_table ).removeClass( 'current' ); + $this_row + .addClass( 'selected_now' ) + .addClass( 'current' ); - if ( $( 'tr.last_selected', $this_table ).length > 0 ) { - if ( $this_row.index() > $( 'tr.last_selected', $this_table ).index() ) { - $( 'tr', $this_table ) - .slice( $( 'tr.last_selected', $this_table ).index(), $this_row.index() ) - .addClass( 'current' ); + if ( $( 'tr.last_selected', $this_table ).length > 0 ) { + if ( + $this_row.index() > + $( 'tr.last_selected', $this_table ).index() + ) { + $( 'tr', $this_table ) + .slice( + $( + 'tr.last_selected', + $this_table + ).index(), + $this_row.index() + ) + .addClass( 'current' ); + } else { + $( 'tr', $this_table ) + .slice( + $this_row.index(), + $( + 'tr.last_selected', + $this_table + ).index() + 1 + ) + .addClass( 'current' ); + } + } + + $( 'tr', $this_table ).removeClass( 'last_selected' ); + $this_row.addClass( 'last_selected' ); + } else { + $( 'tr', $this_table ).removeClass( 'last_selected' ); + if ( + controlled && + $( this ).closest( 'tr' ).is( '.current' ) + ) { + $this_row.removeClass( 'current' ); } else { - $( 'tr', $this_table ) - .slice( $this_row.index(), $( 'tr.last_selected', $this_table ).index() + 1 ) - .addClass( 'current' ); + $this_row + .addClass( 'current' ) + .addClass( 'last_selected' ); } } - $( 'tr', $this_table ).removeClass( 'last_selected' ); - $this_row.addClass( 'last_selected' ); - } else { - $( 'tr', $this_table ).removeClass( 'last_selected' ); - if ( controlled && $( this ).closest( 'tr' ).is( '.current' ) ) { - $this_row.removeClass( 'current' ); - } else { - $this_row.addClass( 'current' ).addClass( 'last_selected' ); - } - } - - $( 'tr', $this_table ).removeClass( 'selected_now' ); - } - }).on( 'blur', 'input', function() { - hasFocus = false; - }); - - // Additional cost and Attribute term tables - $( '.woocommerce_page_wc-settings .shippingrows tbody tr:even, table.attributes-table tbody tr:nth-child(odd)' ) - .addClass( 'alternate' ); - - // Show order items on orders page - $( document.body ).on( 'click', '.show_order_items', function() { - $( this ).closest( 'td' ).find( 'table' ).toggle(); - return false; - }); - - // Select availability - $( 'select.availability' ).on( 'change', function() { - if ( $( this ).val() === 'all' ) { - $( this ).closest( 'tr' ).next( 'tr' ).hide(); - } else { - $( this ).closest( 'tr' ).next( 'tr' ).show(); - } - }).trigger( 'change' ); - - // Hidden options - $( '.hide_options_if_checked' ).each( function() { - $( this ).find( 'input:eq(0)' ).on( 'change', function() { - if ( $( this ).is( ':checked' ) ) { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .hide(); - } else { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .show(); - } - }).trigger( 'change' ); - }); - - $( '.show_options_if_checked' ).each( function() { - $( this ).find( 'input:eq(0)' ).on( 'change', function() { - if ( $( this ).is( ':checked' ) ) { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .show(); - } else { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .hide(); - } - }).trigger( 'change' ); - }); - - // Reviews. - $( 'input#woocommerce_enable_reviews' ).on( 'change', function() { - if ( $( this ).is( ':checked' ) ) { - $( '#woocommerce_enable_review_rating' ).closest( 'tr' ).show(); - } else { - $( '#woocommerce_enable_review_rating' ).closest( 'tr' ).hide(); - } - }).trigger( 'change' ); - - // Attribute term table - $( 'table.attributes-table tbody tr:nth-child(odd)' ).addClass( 'alternate' ); - - // Toggle gateway on/off. - $( '.wc_gateways' ).on( 'click', '.wc-payment-gateway-method-toggle-enabled', function() { - var $link = $( this ), - $row = $link.closest( 'tr' ), - $toggle = $link.find( '.woocommerce-input-toggle' ); - - var data = { - action: 'woocommerce_toggle_gateway_enabled', - security: woocommerce_admin.nonces.gateway_toggle, - gateway_id: $row.data( 'gateway_id' ) - }; - - $toggle.addClass( 'woocommerce-input-toggle--loading' ); - - $.ajax( { - url: woocommerce_admin.ajax_url, - data: data, - dataType : 'json', - type : 'POST', - success: function( response ) { - if ( true === response.data ) { - $toggle.removeClass( 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' ); - $toggle.addClass( 'woocommerce-input-toggle--enabled' ); - $toggle.removeClass( 'woocommerce-input-toggle--loading' ); - } else if ( false === response.data ) { - $toggle.removeClass( 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' ); - $toggle.addClass( 'woocommerce-input-toggle--disabled' ); - $toggle.removeClass( 'woocommerce-input-toggle--loading' ); - } else if ( 'needs_setup' === response.data ) { - window.location.href = $link.attr( 'href' ); - } + $( 'tr', $this_table ).removeClass( 'selected_now' ); } + } ) + .on( 'blur', 'input', function () { + hasFocus = false; } ); - return false; - }); + // Additional cost and Attribute term tables + $( + '.woocommerce_page_wc-settings .shippingrows tbody tr:even, table.attributes-table tbody tr:nth-child(odd)' + ).addClass( 'alternate' ); - $( '#wpbody' ).on( 'click', '#doaction, #doaction2', function() { - var action = $( this ).is( '#doaction' ) ? $( '#bulk-action-selector-top' ).val() : $( '#bulk-action-selector-bottom' ).val(); + // Show order items on orders page + $( document.body ).on( 'click', '.show_order_items', function () { + $( this ).closest( 'td' ).find( 'table' ).toggle(); + return false; + } ); + + // Select availability + $( 'select.availability' ) + .on( 'change', function () { + if ( $( this ).val() === 'all' ) { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + } + } ) + .trigger( 'change' ); + + // Hidden options + $( '.hide_options_if_checked' ).each( function () { + $( this ) + .find( 'input:eq(0)' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .hide(); + } else { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .show(); + } + } ) + .trigger( 'change' ); + } ); + + $( '.show_options_if_checked' ).each( function () { + $( this ) + .find( 'input:eq(0)' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .show(); + } else { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .hide(); + } + } ) + .trigger( 'change' ); + } ); + + // Reviews. + $( 'input#woocommerce_enable_reviews' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( '#woocommerce_enable_review_rating' ) + .closest( 'tr' ) + .show(); + } else { + $( '#woocommerce_enable_review_rating' ) + .closest( 'tr' ) + .hide(); + } + } ) + .trigger( 'change' ); + + // Attribute term table + $( 'table.attributes-table tbody tr:nth-child(odd)' ).addClass( + 'alternate' + ); + + // Toggle gateway on/off. + $( '.wc_gateways' ).on( + 'click', + '.wc-payment-gateway-method-toggle-enabled', + function () { + var $link = $( this ), + $row = $link.closest( 'tr' ), + $toggle = $link.find( '.woocommerce-input-toggle' ); + + var data = { + action: 'woocommerce_toggle_gateway_enabled', + security: woocommerce_admin.nonces.gateway_toggle, + gateway_id: $row.data( 'gateway_id' ), + }; + + $toggle.addClass( 'woocommerce-input-toggle--loading' ); + + $.ajax( { + url: woocommerce_admin.ajax_url, + data: data, + dataType: 'json', + type: 'POST', + success: function ( response ) { + if ( true === response.data ) { + $toggle.removeClass( + 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' + ); + $toggle.addClass( + 'woocommerce-input-toggle--enabled' + ); + $toggle.removeClass( + 'woocommerce-input-toggle--loading' + ); + } else if ( false === response.data ) { + $toggle.removeClass( + 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' + ); + $toggle.addClass( + 'woocommerce-input-toggle--disabled' + ); + $toggle.removeClass( + 'woocommerce-input-toggle--loading' + ); + } else if ( 'needs_setup' === response.data ) { + window.location.href = $link.attr( 'href' ); + } + }, + } ); + + return false; + } + ); + + $( '#wpbody' ).on( 'click', '#doaction, #doaction2', function () { + var action = $( this ).is( '#doaction' ) + ? $( '#bulk-action-selector-top' ).val() + : $( '#bulk-action-selector-bottom' ).val(); if ( 'remove_personal_data' === action ) { - return window.confirm( woocommerce_admin.i18n_remove_personal_data_notice ); + return window.confirm( + woocommerce_admin.i18n_remove_personal_data_notice + ); } - }); + } ); - var marketplaceSectionDropdown = $( '#marketplace-current-section-dropdown' ); + var marketplaceSectionDropdown = $( + '#marketplace-current-section-dropdown' + ); var marketplaceSectionName = $( '#marketplace-current-section-name' ); var marketplaceMenuIsOpen = false; // Add event listener to toggle Marketplace menu on touch devices if ( marketplaceSectionDropdown.length ) { if ( isTouchDevice() ) { - marketplaceSectionName.on( 'click', function() { + marketplaceSectionName.on( 'click', function () { marketplaceMenuIsOpen = ! marketplaceMenuIsOpen; if ( marketplaceMenuIsOpen ) { marketplaceSectionDropdown.addClass( 'is-open' ); $( document ).on( 'click', maybeToggleMarketplaceMenu ); } else { marketplaceSectionDropdown.removeClass( 'is-open' ); - $( document ).off( 'click', maybeToggleMarketplaceMenu ); + $( document ).off( + 'click', + maybeToggleMarketplaceMenu + ); } } ); } else { @@ -438,8 +643,8 @@ // Close menu if the user clicks outside it function maybeToggleMarketplaceMenu( e ) { if ( - ! marketplaceSectionDropdown.is( e.target ) - && marketplaceSectionDropdown.has( e.target ).length === 0 + ! marketplaceSectionDropdown.is( e.target ) && + marketplaceSectionDropdown.has( e.target ).length === 0 ) { marketplaceSectionDropdown.removeClass( 'is-open' ); marketplaceMenuIsOpen = false; @@ -448,11 +653,11 @@ } function isTouchDevice() { - return ( ( 'ontouchstart' in window ) || - ( navigator.maxTouchPoints > 0 ) || - ( navigator.msMaxTouchPoints > 0 ) ); + return ( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ); } - - }); - -})( jQuery, woocommerce_admin ); + } ); +} )( jQuery, woocommerce_admin ); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index 9dee92915e0..97e8bb32cc4 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -84,7 +84,9 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : // @deprecated 2.3. if ( has_action( 'woocommerce_admin_css' ) ) { + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_admin_css' ); + /* phpcs: enable */ wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' ); } @@ -182,15 +184,15 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_enqueue_script( 'jquery-ui-sortable' ); wp_enqueue_script( 'jquery-ui-autocomplete' ); - $locale = localeconv(); + $locale = localeconv(); $decimal_point = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; - $decimal = ( ! empty( wc_get_price_decimal_separator() ) ) ? wc_get_price_decimal_separator() : $decimal_point; + $decimal = ( ! empty( wc_get_price_decimal_separator() ) ) ? wc_get_price_decimal_separator() : $decimal_point; $params = array( /* translators: %s: decimal */ - 'i18n_decimal_error' => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), + 'i18n_decimal_error' => sprintf( __( 'Please enter a value with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), /* translators: %s: price decimal separator */ - 'i18n_mon_decimal_error' => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), + 'i18n_mon_decimal_error' => sprintf( __( 'Please enter a value with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), 'i18n_country_iso_error' => __( 'Please enter in country code with two capital letters.', 'woocommerce' ), 'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ), 'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ), @@ -247,10 +249,11 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : } // Meta boxes. + /* phpcs:disable */ if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { wp_enqueue_media(); wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version ); - wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version ); + wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version ); wp_enqueue_script( 'wc-admin-product-meta-boxes' ); wp_enqueue_script( 'wc-admin-variation-meta-boxes' ); @@ -289,6 +292,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params ); } + /* phpcs: enable */ if ( $this->is_order_meta_box_screen( $screen_id ) ) { $default_location = wc_get_customer_default_location(); @@ -306,6 +310,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : ) ); } + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) { wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version ); wp_localize_script( @@ -320,6 +325,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : ) ); } + /* phpcs: enable */ if ( in_array( str_replace( 'edit-', '', $screen_id ), array( 'shop_coupon', 'product' ), true ) || $this->is_order_meta_box_screen( $screen_id ) ) { $post_id = isset( $post->ID ) ? $post->ID : ''; $currency = ''; @@ -409,6 +415,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : } // Term ordering - only when sorting by term_order. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) { wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version ); @@ -422,6 +429,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params ); } + /* phpcs: enable */ // Product sorting - only when sorting by menu order on the products page. if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) { @@ -430,6 +438,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : } // Reports Pages. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) { wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version ); @@ -440,6 +449,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_enqueue_script( 'flot-pie' ); wp_enqueue_script( 'flot-stack' ); } + /* phpcs: enable */ // API settings. if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php index a7751b42284..560468487b4 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php @@ -53,6 +53,7 @@ class WC_Meta_Box_Product_Data { * @return array */ private static function get_product_type_options() { + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ return apply_filters( 'product_type_options', array( @@ -72,6 +73,7 @@ class WC_Meta_Box_Product_Data { ), ) ); + /* phpcs: enable */ } /** @@ -80,6 +82,7 @@ class WC_Meta_Box_Product_Data { * @return array */ private static function get_product_data_tabs() { + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $tabs = apply_filters( 'woocommerce_product_data_tabs', array( @@ -127,6 +130,7 @@ class WC_Meta_Box_Product_Data { ), ) ); + /* phpcs: enable */ // Sort tabs based on priority. uasort( $tabs, array( __CLASS__, 'product_data_tabs_sort' ) ); @@ -171,11 +175,14 @@ class WC_Meta_Box_Product_Data { public static function output_variations() { global $post, $wpdb, $product_object; + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); $default_attributes = $product_object->get_default_attributes(); $variations_count = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) ); $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); $variations_total_pages = ceil( $variations_count / $variations_per_page ); + $modal_title = get_bloginfo( 'name' ) . __( ' says', 'woocommerce' ); + /* phpcs: enable */ include __DIR__ . '/views/html-product-data-variations.php'; } @@ -272,7 +279,9 @@ class WC_Meta_Box_Product_Data { $attribute->set_position( $attribute_position[ $i ] ); $attribute->set_visible( isset( $attribute_visibility[ $i ] ) ); $attribute->set_variation( isset( $attribute_variation[ $i ] ) ); + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $attributes[] = apply_filters( 'woocommerce_admin_meta_boxes_prepare_attribute', $attribute, $data, $i ); + /* phpcs: enable */ } } return $attributes; @@ -425,9 +434,9 @@ class WC_Meta_Box_Product_Data { $product->get_data_store()->sync_variation_names( $product, $original_post_title, $post_title ); } - + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id ); - // phpcs:enable WordPress.Security.NonceVerification.Missing + /* phpcs:enable WordPress.Security.NonceVerification.Missing and WooCommerce.Commenting.CommentHooks.MissingHookComment */ } /** @@ -448,7 +457,7 @@ class WC_Meta_Box_Product_Data { $max_loop = max( array_keys( wp_unslash( $_POST['variable_post_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $data_store = $parent->get_data_store(); $data_store->sort_all_product_variations( $parent->get_id() ); - $new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false; + $new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false; $new_variation_menu_order_value = ! empty( $_POST['new_variation_menu_order_value'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_value'] ) ) : false; // Only perform this operation if setting menu order via the prompt. @@ -560,7 +569,9 @@ class WC_Meta_Box_Product_Data { do_action( 'woocommerce_admin_process_variation_object', $variation, $i ); $variation->save(); + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_save_product_variation', $variation_id, $i ); + /* phpcs: enable */ } } // phpcs:enable WordPress.Security.NonceVerification.Missing diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php index 5bff4c42b5a..6f69d8bd31b 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php @@ -16,7 +16,9 @@ if ( ! defined( 'ABSPATH' ) ) {

Attributes tab.', 'woocommerce' ) ); ?>

+

+
@@ -33,11 +35,15 @@ if ( ! defined( 'ABSPATH' ) ) { is_taxonomy() ) : ?> get_terms() as $option ) : ?> + + get_options() as $option ) : ?> + + @@ -48,7 +54,9 @@ if ( ! defined( 'ABSPATH' ) ) {
+ +
@@ -104,7 +114,9 @@ if ( ! defined( 'ABSPATH' ) ) { @@ -116,7 +128,9 @@ if ( ! defined( 'ABSPATH' ) ) {
+
+
@@ -135,7 +149,9 @@ if ( ! defined( 'ABSPATH' ) ) { @@ -150,3 +166,25 @@ if ( ! defined( 'ABSPATH' ) ) {
+ diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php index 5402c3576d1..45202b0e407 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php @@ -14,8 +14,8 @@ defined( 'ABSPATH' ) || exit; ?>

+ -
# is_taxonomy() ) : ?> get_terms() as $option ) : ?> + + get_options() as $option ) : ?> + + @@ -106,7 +110,9 @@ defined( 'ABSPATH' ) || exit; + +

@@ -152,6 +158,7 @@ defined( 'ABSPATH' ) || exit; $sale_price_dates_from = $sale_price_dates_from_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_from_timestamp ) : ''; $sale_price_dates_to = $sale_price_dates_to_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_to_timestamp ) : ''; + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ echo ''; + /* phpcs: enable */ /** * Variation options pricing action. @@ -236,7 +244,7 @@ defined( 'ABSPATH' ) || exit; 'custom_attributes' => array( 'step' => 'any', ), - 'wrapper_class' => 'form-row', + 'wrapper_class' => 'form-row', ) ); @@ -409,7 +417,7 @@ defined( 'ABSPATH' ) || exit; if ( $downloadable_files ) { foreach ( $downloadable_files as $key => $file ) { - $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; + $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; $disabled_downloads_count += (int) $disabled_download; include __DIR__ . '/html-product-variation-download.php'; } @@ -421,8 +429,8 @@ defined( 'ABSPATH' ) || exit; posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_name = %s LIMIT 1;", $slug ) ); } + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $valid_page_found = apply_filters( 'woocommerce_create_page_id', $valid_page_found, $slug, $page_content ); + /* phpcs: enable */ if ( $valid_page_found ) { if ( $option ) { @@ -145,7 +149,9 @@ function wc_create_page( $slug, $option = '', $page_title = '', $page_content = ); $page_id = wp_insert_post( $page_data ); + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_page_created', $page_id, $page_data ); + /* phpcs: enable */ } if ( $option ) { @@ -287,7 +293,9 @@ function wc_maybe_adjust_line_item_product_stock( $item, $item_quantity = -1 ) { */ function wc_save_order_items( $order_id, $items ) { // Allow other plugins to check change in order items before they are saved. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_before_save_order_items', $order_id, $items ); + /* phpcs: enable */ $qty_change_order_notes = array(); $order = wc_get_order( $order_id ); @@ -361,7 +369,9 @@ function wc_save_order_items( $order_id, $items ) { } // Allow other plugins to change item object before it is saved. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_before_save_order_item', $item ); + /* phpcs: enable */ $item->save(); @@ -438,7 +448,9 @@ function wc_save_order_items( $order_id, $items ) { $order->calculate_totals( false ); // Inform other plugins that the items have been saved. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_saved_order_items', $order_id, $items ); + /* phpcs: enable */ } /** @@ -472,9 +484,11 @@ function wc_render_invalid_variation_notice( $product_object ) { global $wpdb; // Give ability for extensions to hide this notice. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( ! apply_filters( 'woocommerce_show_invalid_variations_notice', true, $product_object ) ) { return; } + /* phpcs: enable */ $variation_ids = $product_object ? $product_object->get_children() : array(); @@ -490,8 +504,8 @@ function wc_render_invalid_variation_notice( $product_object ) { " SELECT count(post_id) FROM {$wpdb->postmeta} WHERE post_id in (" . implode( ',', array_map( 'absint', $variation_ids ) ) . ") - AND meta_key='_price' - AND meta_value >= 0 + AND ( meta_key='_subscription_sign_up_fee' OR meta_key='_price' ) + AND meta_value > 0 AND meta_value != '' " ); @@ -499,7 +513,7 @@ function wc_render_invalid_variation_notice( $product_object ) { if ( 0 < ( $variation_count - $invalid_variation_count ) ) { ?> -
+

+
+ +
Date: Wed, 2 Nov 2022 14:00:17 +0000 Subject: [PATCH 125/149] Update/a2p array checks in api-core-tests (#35462) * Update array checks * add changelog file --- .../changelog/update-array-checks-api-core-tests | 4 ++++ .../api-core-tests/tests/customers/customers-crud.test.js | 6 +++--- .../tests/api-core-tests/tests/orders/orders-crud.test.js | 2 +- .../api-core-tests/tests/shipping/shipping-method.test.js | 2 +- .../api-core-tests/tests/taxes/tax-classes-crud.test.js | 2 +- .../tests/api-core-tests/tests/taxes/tax-rates-crud.test.js | 2 +- .../api-core-tests/tests/webhooks/webhooks-crud.test.js | 2 +- 7 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-array-checks-api-core-tests diff --git a/plugins/woocommerce/changelog/update-array-checks-api-core-tests b/plugins/woocommerce/changelog/update-array-checks-api-core-tests new file mode 100644 index 00000000000..a3f9948da49 --- /dev/null +++ b/plugins/woocommerce/changelog/update-array-checks-api-core-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Array checks in playwright api-core-tests as some of the existing tests would produce false positives \ No newline at end of file diff --git a/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js index b241e223240..6234bf7e155 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js @@ -67,7 +67,7 @@ test.describe('Customers API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/customers'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toEqual(0); }); @@ -85,7 +85,7 @@ test.describe('Customers API tests: CRUD', () => { }); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThanOrEqual(2); }); }); @@ -132,7 +132,7 @@ test.describe('Customers API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/customers'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js index 5b50e8b5ee1..61a8f681f06 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js @@ -175,7 +175,7 @@ test.describe('Orders API tests: CRUD', () => { const response = await request.get(`/wp-json/wc/v3/orders/${orderId}/notes`); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js index 5a66200f908..51f52e70144 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js @@ -65,7 +65,7 @@ test.describe('Shipping methods API tests', () => { const response = await request.get('/wp-json/wc/v3/shipping_methods'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toEqual(3); expect(responseJSON[0].id).toEqual("flat_rate"); expect(responseJSON[1].id).toEqual("free_shipping"); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js index 4396bf9a631..353a3dada79 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js @@ -74,7 +74,7 @@ test.describe('Tax Classes API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/taxes/classes'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js index 1d0a636a242..fcc339ba3e2 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js @@ -89,7 +89,7 @@ test.describe('Tax Rates API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/taxes'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js index ff5895281d8..12993f5d539 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js @@ -82,7 +82,7 @@ test.describe('Webhooks API tests', () => { const response = await request.get('/wp-json/wc/v3/webhooks'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); From 3fc0ee338e77e1a4319cb702ac40a61462238703 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 3 Nov 2022 02:40:02 +0800 Subject: [PATCH 126/149] Fix business details step fails to display when Gutenberg plugin is active (#35448) * Fix business details tabs when Gutenberg is active Fix business details tabs when Gutenberg is active Fix business details tabs when Gutenberg is active * Add changelog --- .../steps/business-details/flows/selective-bundle/index.js | 6 +++++- .../changelog/fix-35417-bussiness-details-with-gutenberg | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js index 26d24a79218..08e18e6bd38 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js @@ -766,7 +766,11 @@ class BusinessDetails extends Component { activeClass="is-active" initialTabName="current-tab" onSelect={ ( tabName ) => { - if ( this.state.currentTab !== tabName ) { + if ( + this.state.currentTab !== tabName && + // TabPanel calls onSelect on mount when initialTabName is provided, so we need to check if the tabName is valid. + tabName !== 'current-tab' + ) { this.setState( { currentTab: tabName, savedValues: diff --git a/plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg b/plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg new file mode 100644 index 00000000000..50a3c180825 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix business details step when Gutenberg is active From e92f95903b1c3376f4d39a34056cd663fa818c01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Nov 2022 13:56:40 -0500 Subject: [PATCH 127/149] Delete changelog files based on PR 35448 (#35475) Delete changelog files for 35448 Co-authored-by: WooCommerce Bot --- .../changelog/fix-35417-bussiness-details-with-gutenberg | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg diff --git a/plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg b/plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg deleted file mode 100644 index 50a3c180825..00000000000 --- a/plugins/woocommerce/changelog/fix-35417-bussiness-details-with-gutenberg +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix business details step when Gutenberg is active From 3df6dcd6be817c9e4f0811a83537d3a603a23f8b Mon Sep 17 00:00:00 2001 From: "Saif H. Hassan" <67080558+Babylon1999@users.noreply.github.com> Date: Thu, 3 Nov 2022 02:28:47 +0200 Subject: [PATCH 128/149] Libyan Dinar not formatted correctly (#35395) Update and correct the symbol for Libyan Dinars. * Update wc-core-functions.php * Update settings.js * Add changelog. * Update API test `can view all currencies` re Libyan Dinar. Co-authored-by: barryhughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/changelog/libyan-dinar | 4 ++++ plugins/woocommerce/includes/wc-core-functions.php | 2 +- plugins/woocommerce/tests/api-core-tests/data/settings.js | 2 +- .../tests/api-core-tests/tests/data/data-crud.test.js | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/libyan-dinar diff --git a/plugins/woocommerce/changelog/libyan-dinar b/plugins/woocommerce/changelog/libyan-dinar new file mode 100644 index 00000000000..c97817a14c0 --- /dev/null +++ b/plugins/woocommerce/changelog/libyan-dinar @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Corrects the currency symbol for Libyan Dinar (LYD). diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php index 63e8ee2e2b5..13fc2bee29f 100644 --- a/plugins/woocommerce/includes/wc-core-functions.php +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -752,7 +752,7 @@ function get_woocommerce_currency_symbols() { 'LKR' => 'රු', 'LRD' => '$', 'LSL' => 'L', - 'LYD' => 'ل.د', + 'LYD' => 'د.ل', 'MAD' => 'د.م.', 'MDL' => 'MDL', 'MGA' => 'Ar', diff --git a/plugins/woocommerce/tests/api-core-tests/data/settings.js b/plugins/woocommerce/tests/api-core-tests/data/settings.js index 925eddac81b..6ab0c22f2f7 100644 --- a/plugins/woocommerce/tests/api-core-tests/data/settings.js +++ b/plugins/woocommerce/tests/api-core-tests/data/settings.js @@ -96,7 +96,7 @@ const currencies = { "LKR": "Sri Lankan rupee (රු)", "LRD": "Liberian dollar ($)", "LSL": "Lesotho loti (L)", - "LYD": "Libyan dinar (ل.د)", + "LYD": "Libyan dinar (د.ل)", "MAD": "Moroccan dirham (د.م.)", "MDL": "Moldovan leu (MDL)", "MGA": "Malagasy ariary (Ar)", diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js index bcf5d752d9d..824c248d6bb 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js @@ -26206,7 +26206,7 @@ test.describe('Data API tests', () => { expect.objectContaining({ "code": "LYD", "name": "Libyan dinar", - "symbol": "ل.د", + "symbol": "د.ل", "_links": { "self": [{ "href": expect.stringContaining("data/currencies/LYD") From 76f99a482f6d05094078219225f896db9113f7d3 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 3 Nov 2022 09:22:36 +0800 Subject: [PATCH 129/149] Cleanup and deprecate task properties and methods (#35450) * Remove unused sectioned task code * Remove section task type and update getVisibleTasks logic * Clean up task list and deprecate methods/properties * Add changelog * Fix lint * Remove snooze tests * Remove snooze JS tests --- ...ev-clean-up-unused-task-properties-methods | 4 + packages/js/data/src/onboarding/types.ts | 10 - packages/js/data/src/onboarding/utils.ts | 6 +- .../woocommerce-admin/client/tasks/tasks.tsx | 4 +- .../client/tasks/test/task-list.test.tsx | 52 ----- .../headers/section-header.scss | 36 --- .../headers/section-header.tsx | 30 --- .../two-column-tasks/section-panel-title.tsx | 58 ----- .../sectioned-task-list-placeholder.tsx | 75 ------- .../two-column-tasks/sectioned-task-list.scss | 165 -------------- .../two-column-tasks/sectioned-task-list.tsx | 209 ------------------ .../two-column-tasks/task-list-item.tsx | 189 ---------------- .../two-column-tasks/test/task-list.test.tsx | 52 ----- ...ev-clean-up-unused-task-properties-methods | 4 + .../Admin/Features/OnboardingTasks/Init.php | 6 - .../Admin/Features/OnboardingTasks/Task.php | 41 +++- .../Features/OnboardingTasks/TaskList.php | 25 +-- .../OnboardingTasks/TaskListSection.php | 2 + .../OnboardingTasks/Tasks/Appearance.php | 6 - .../OnboardingTasks/Tasks/Marketing.php | 6 - .../OnboardingTasks/Tasks/Payments.php | 7 - .../OnboardingTasks/Tasks/Products.php | 6 - .../OnboardingTasks/Tasks/Shipping.php | 6 - .../Features/OnboardingTasks/Tasks/Tax.php | 6 - .../api/onboarding-tasks.php | 180 --------------- .../features/onboarding-tasks/task.php | 94 -------- 26 files changed, 57 insertions(+), 1222 deletions(-) create mode 100644 packages/js/data/changelog/dev-clean-up-unused-task-properties-methods delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx delete mode 100644 plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx create mode 100644 plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods diff --git a/packages/js/data/changelog/dev-clean-up-unused-task-properties-methods b/packages/js/data/changelog/dev-clean-up-unused-task-properties-methods new file mode 100644 index 00000000000..49ebaf5e3ee --- /dev/null +++ b/packages/js/data/changelog/dev-clean-up-unused-task-properties-methods @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update task list types and getVisibleTasks logic diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts index 15c9bd5fc27..24060daa19d 100644 --- a/packages/js/data/src/onboarding/types.ts +++ b/packages/js/data/src/onboarding/types.ts @@ -53,15 +53,6 @@ export type DeprecatedTaskType = { type?: string; }; -export type TaskListSection = { - id: string; - title: string; - description: string; - image: string; - tasks: string[]; - isComplete: boolean; -}; - export type TaskListType = { id: string; title: string; @@ -73,7 +64,6 @@ export type TaskListType = { displayProgressHeader: boolean; keepCompletedTaskList: 'yes' | 'no'; showCESFeedback?: boolean; - sections?: TaskListSection[]; isToggleable?: boolean; isCollapsible?: boolean; isExpandable?: boolean; diff --git a/packages/js/data/src/onboarding/utils.ts b/packages/js/data/src/onboarding/utils.ts index 9623eeff560..3fbca5364d2 100644 --- a/packages/js/data/src/onboarding/utils.ts +++ b/packages/js/data/src/onboarding/utils.ts @@ -7,8 +7,4 @@ import { TaskType } from './types'; * Filters tasks to only visible tasks, taking in account snoozed tasks. */ export const getVisibleTasks = ( tasks: TaskType[] ) => - tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < Date.now() ) - ); + tasks.filter( ( task ) => ! task.isDismissed ); diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 7c6368a2fa2..84dd5c6ec65 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -20,15 +20,13 @@ import { recordEvent } from '@woocommerce/tracks'; */ import { DisplayOption } from '~/activity-panel/display-options'; import { Task } from './task'; -import { TasksPlaceholder, TasksPlaceholderProps } from './placeholder'; +import { TasksPlaceholder } from './placeholder'; import './tasks.scss'; import { TaskList } from './task-list'; import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; -import { SectionedTaskList } from '../two-column-tasks/sectioned-task-list'; import TwoColumnTaskListPlaceholder from '../two-column-tasks/placeholder'; import '../two-column-tasks/style.scss'; import { getAdminSetting } from '~/utils/admin-settings'; -import { SectionedTaskListPlaceholder } from '~/two-column-tasks/sectioned-task-list-placeholder'; export type TasksProps = { query: { task?: string }; diff --git a/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx b/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx index fe6cd2e29b7..169c7b42b6e 100644 --- a/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx +++ b/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx @@ -235,56 +235,4 @@ describe( 'TaskList', () => { queryByText( dismissedTask[ 0 ].title ) ).not.toBeInTheDocument(); } ); - - it( 'should not display isSnoozed tasks', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() + 10000, - }, - ]; - const { queryByText } = render( - - ); - expect( - queryByText( dismissedTask[ 0 ].title ) - ).not.toBeInTheDocument(); - } ); - - it( 'should display a snoozed task if snoozedUntil passed the current timestamp', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() - 1000, - }, - ]; - const { queryByText } = render( - - ); - expect( queryByText( dismissedTask[ 0 ].title ) ).toBeInTheDocument(); - } ); } ); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss deleted file mode 100644 index 9a0ab073071..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss +++ /dev/null @@ -1,36 +0,0 @@ -.woocommerce-task-section-header__container { - display: flex; - - .woocommerce-task-header__illustration { - max-width: 150px; - width: 34%; - margin-left: auto; - margin-right: 7%; - display: flex; - align-items: center; - - .illustration-background { - max-width: 100%; - } - } - - @at-root .woocommerce-setup-panel & .woocommerce-task-header__contents p { - font-size: 13px; - } - - - .woocommerce-task-header__contents p { - font-size: 16px; - } - - @at-root .woocommerce-setup-panel & .woocommerce-task-header__contents h1 { - font-size: 14px; - font-weight: 600; - } - - .woocommerce-task-header__contents h1 { - font-size: 20px; - line-height: 28px; - padding: 0; - } -} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx deleted file mode 100644 index c84a892084e..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Internal dependencies - */ -import './section-header.scss'; - -type Props = { - title: string; - description: string; - image: string; -}; - -const SectionHeader: React.FC< Props > = ( { title, description, image } ) => { - return ( -
-
-

{ title }

-

{ description }

-
-
- { -
-
- ); -}; - -export default SectionHeader; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx deleted file mode 100644 index c2e0927d0b7..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * External dependencies - */ -import { Badge } from '@woocommerce/components'; -import { TaskListSection, TaskType } from '@woocommerce/data'; -import { Icon, check } from '@wordpress/icons'; -import { Text } from '@woocommerce/experimental'; - -/** - * Internal dependencies - */ -import SectionHeader from './headers/section-header'; - -type SectionPanelTitleProps = { - section: TaskListSection; - active: boolean; - tasks: TaskType[]; -}; - -export const SectionPanelTitle: React.FC< SectionPanelTitleProps > = ( { - section, - active, - tasks, -} ) => { - if ( active ) { - return ( -
-
- -
-
- ); - } - - const uncompletedTasksCount = tasks.filter( - ( task ) => ! task.isComplete && section.tasks.includes( task.id ) - ).length; - const isComplete = section.isComplete || uncompletedTasksCount === 0; - - return ( - <> - - { section.title } - - { ! isComplete && } - { isComplete && ( -
- -
- ) } - - ); -}; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx deleted file mode 100644 index 70b234eaf0a..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Internal dependencies - */ -import './style.scss'; - -type TasksPlaceholderProps = { - numTasks?: number; - query: { - task?: string; - }; -}; - -const SectionedTaskListPlaceholder: React.FC< TasksPlaceholderProps > = ( - props -) => { - const { numTasks = 3 } = props; - - return ( -
-
-
-
-
-
-
-
    - { Array.from( new Array( numTasks ) ).map( ( v, i ) => ( -
  • -
    -
    -
    -
    -
    -
    -
  • - ) ) } -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -}; - -export { SectionedTaskListPlaceholder }; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss deleted file mode 100644 index a55dea2a5aa..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ /dev/null @@ -1,165 +0,0 @@ -.woocommerce-sectioned-task-list { - .components-panel { - width: 100%; - background: transparent; - border: 0; - } - - .components-panel__body { - padding-bottom: 0; - margin-bottom: $gap-smaller; - background: #fff; - border: 1px solid $gray-200; - - &.is-opened { - padding-bottom: 0; - } - - .components-panel__body-title { - margin-bottom: 0; - border-bottom: 1px solid #e0e0e0; - - &:hover { - border-bottom: 1px solid #e0e0e0; - } - - @at-root .woocommerce-setup-panel & > .components-button { - font-size: 14px; - } - - > .components-button { - font-size: 20px; - font-weight: 400; - padding-top: 20px; - padding-bottom: 20px; - } - - .components-panel__arrow { - right: $gap-large; - } - - .woocommerce-task-header__contents p:first-of-type { - margin-top: $gap-small; - } - } - .wooocommerce-task-card__header-container { - width: 100%; - border-bottom: none; - } - .components-panel__body-toggle { - box-shadow: none; - padding-left: $gap-large; - } - &.is-opened .components-panel__body-toggle { - width: 100%; - padding: 0; - .components-panel__arrow { - top: 32px; - } - } - - .woocommerce-experimental-list { - width: calc(100% + 32px); - margin: 0 -16px; - } - } - ul li.woocommerce-task-list__item { - padding-top: $gap; - padding-bottom: $gap; - min-height: 72px; - - &.is-disabled { - pointer-events: none; - } - - &:not(.complete) - .woocommerce-task-list__item-before - .woocommerce-task__icon { - border-color: $gray-300; - } - .woocommerce-task-list__item-expandable-content { - line-height: $gap; - } - } - - .woocommerce-task-list__item.complete .woocommerce-task__icon { - background-color: $alert-green; - } - - .components-panel__body-title { - .woocommerce-badge { - width: 28px; - height: 28px; - margin-left: $gap-small; - } - .woocommerce-task__icon { - margin-left: $gap; - background-color: $alert-green; - border-radius: 50%; - width: 24px; - height: 24px; - svg { - fill: #fff; - position: relative; - } - } - } - - > .is-loading { - border: none; - margin-bottom: 8px; - - .woocommerce-task-list__item .woocommerce-task-list__item-before { - padding: 0 0 0 $gap-large; - } - - &.components-panel__body .components-panel__body-title .woocommerce-task-list__item-text { - width: 50%; - - .is-placeholder { - width: 100%; - } - } - - &.components-panel__body .woocommerce-task-list__item-after { - margin-left: $gap; - - .is-placeholder { - height: 24px; - width: 24px; - border-radius: 50%; - } - } - } -} - -.woocommerce-setup-panel { - .two-column-experiment { - h1 { - font-size: 16px; - font-weight: 600; - } - } - - .woocommerce-task-header-collapsed { - font-size: 14px; - line-height: 20px; - font-weight: bold; - } - - .woocommerce-task-progress-header { - padding: $gap; - margin-bottom: $gap-smaller; - background: #fff; - border: 1px solid $gray-200; - - .woocommerce-task-progress-header__title { - padding-top: 4px; - } - - .woocommerce-ellipsis-menu { - display: none; - } - - } -} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx deleted file mode 100644 index 147757bdae4..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useRef, useState, useContext } from '@wordpress/element'; -import { Panel, PanelBody, PanelRow } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, getVisibleTasks } from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; -import { List } from '@woocommerce/experimental'; -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import '../tasks/task-list.scss'; -import './sectioned-task-list.scss'; -import TaskListCompleted from './completed'; -import { TaskListProps } from '~/tasks/task-list'; -import { ProgressHeader } from '~/task-lists/progress-header'; -import { SectionPanelTitle } from './section-panel-title'; -import { TaskListItem } from './task-list-item'; -import { TaskListCompletedHeader } from './completed-header'; -import { LayoutContext } from '~/layout'; - -type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { - title: string | React.ReactNode | undefined; - onToggle?: ( isOpen: boolean ) => void; -}; -const PanelBodyWithUpdatedType = - PanelBody as React.ComponentType< PanelBodyProps >; - -export const SectionedTaskList: React.FC< TaskListProps > = ( { - query, - id, - eventPrefix, - tasks, - keepCompletedTaskList, - isComplete, - sections, - displayProgressHeader, - cesHeader = true, -} ) => { - const { profileItems } = useSelect( ( select ) => { - const { getProfileItems } = select( ONBOARDING_STORE_NAME ); - return { - profileItems: getProfileItems(), - }; - } ); - const { hideTaskList, keepCompletedTaskList: keepCompletedTasks } = - useDispatch( ONBOARDING_STORE_NAME ); - const [ openPanel, setOpenPanel ] = useState< string | null >( - sections?.find( ( section ) => ! section.isComplete )?.id || null - ); - const layoutContext = useContext( LayoutContext ); - - const prevQueryRef = useRef( query ); - - const visibleTasks = getVisibleTasks( tasks ); - - const recordTaskListView = () => { - if ( query.task ) { - return; - } - - recordEvent( `${ eventPrefix }view`, { - number_tasks: visibleTasks.length, - store_connected: profileItems.wccom_connected, - context: layoutContext.toString(), - } ); - }; - - useEffect( () => { - recordTaskListView(); - }, [] ); - - useEffect( () => { - const { task: prevTask } = prevQueryRef.current; - const { task } = query; - - if ( prevTask !== task ) { - window.document.documentElement.scrollTop = 0; - prevQueryRef.current = query; - } - }, [ query ] ); - - const hideTasks = () => { - hideTaskList( id ); - }; - - const keepTasks = () => { - keepCompletedTasks( id ); - }; - - let selectedHeaderCard = visibleTasks.find( - ( listTask ) => listTask.isComplete === false - ); - - // If nothing is selected, default to the last task since everything is completed. - if ( ! selectedHeaderCard ) { - selectedHeaderCard = visibleTasks[ visibleTasks.length - 1 ]; - } - - const getSectionTasks = ( sectionTaskIds: string[] ) => { - return visibleTasks.filter( ( task ) => - sectionTaskIds.includes( task.id ) - ); - }; - - if ( ! visibleTasks.length ) { - return
; - } - - if ( isComplete && keepCompletedTaskList !== 'yes' ) { - return ( - <> - { cesHeader ? ( - - ) : ( - - ) } - - ); - } - - return ( - <> - { displayProgressHeader ? ( - - ) : null } -
- - { ( sections || [] ).map( ( section ) => ( - - } - opened={ openPanel === section.id } - onToggle={ ( isOpen: boolean ) => { - if ( ! isOpen && openPanel === section.id ) { - recordEvent( - `${ eventPrefix }section_closed`, - { - id: section.id, - all: true, - } - ); - setOpenPanel( null ); - } else { - if ( openPanel ) { - recordEvent( - `${ eventPrefix }section_closed`, - { - id: openPanel, - all: false, - } - ); - } - setOpenPanel( section.id ); - } - if ( isOpen ) { - recordEvent( - `${ eventPrefix }section_opened`, - { - id: section.id, - } - ); - } - } } - initialOpen={ false } - > - - - { getSectionTasks( section.tasks ).map( - ( task ) => ( - - ) - ) } - - - - ) ) } - -
- - ); -}; - -export default SectionedTaskList; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx deleted file mode 100644 index bc280ce4a10..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { getNewPath, navigateTo } from '@woocommerce/navigation'; -import { - ONBOARDING_STORE_NAME, - TaskType, - useUserPreferences, -} from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; -import { TaskItem, useSlot } from '@woocommerce/experimental'; -import { useCallback, useContext } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; -import { WooOnboardingTaskListItem } from '@woocommerce/onboarding'; -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import { LayoutContext } from '~/layout'; - -export type TaskListItemProps = { - task: TaskType; - eventPrefix?: string; -}; - -export const TaskListItem: React.FC< TaskListItemProps > = ( { - task, - eventPrefix, -} ) => { - const { createNotice } = useDispatch( 'core/notices' ); - - const { - visitedTask, - dismissTask, - undoDismissTask, - snoozeTask, - undoSnoozeTask, - } = useDispatch( ONBOARDING_STORE_NAME ); - - const layoutContext = useContext( LayoutContext ); - - const slot = useSlot( - `woocommerce_onboarding_task_list_item_${ task.id }` - ); - const hasFills = Boolean( slot?.fills?.length ); - - const userPreferences = useUserPreferences(); - - const getTaskStartedCount = () => { - const trackedStartedTasks = - userPreferences.task_list_tracked_started_tasks; - if ( ! trackedStartedTasks || ! trackedStartedTasks[ task.id ] ) { - return 0; - } - return trackedStartedTasks[ task.id ]; - }; - - const updateTrackStartedCount = () => { - const newCount = getTaskStartedCount() + 1; - const trackedStartedTasks = - userPreferences.task_list_tracked_started_tasks || {}; - - visitedTask( task.id ); - userPreferences.updateUserPreferences( { - task_list_tracked_started_tasks: { - ...( trackedStartedTasks || {} ), - [ task.id ]: newCount, - }, - } ); - }; - - const trackClick = () => { - recordEvent( `${ eventPrefix }click`, { - task_name: task.id, - context: layoutContext.toString(), - } ); - - if ( ! task.isComplete ) { - updateTrackStartedCount(); - } - }; - - const onTaskSelected = () => { - trackClick(); - - if ( task.actionUrl ) { - navigateTo( { - url: task.actionUrl, - } ); - return; - } - - navigateTo( { url: getNewPath( { task: task.id }, '/', {} ) } ); - }; - - const onDismiss = useCallback( () => { - dismissTask( task.id ); - createNotice( 'success', __( 'Task dismissed', 'woocommerce' ), { - actions: [ - { - label: __( 'Undo', 'woocommerce' ), - onClick: () => undoDismissTask( task.id ), - }, - ], - } ); - }, [ task.id ] ); - - const onSnooze = useCallback( () => { - snoozeTask( task.id ); - createNotice( - 'success', - __( 'Task postponed until tomorrow', 'woocommerce' ), - { - actions: [ - { - label: __( 'Undo', 'woocommerce' ), - onClick: () => undoSnoozeTask( task.id ), - }, - ], - } - ); - }, [ task.id ] ); - - const className = classnames( 'woocommerce-task-list__item', { - complete: task.isComplete, - 'is-disabled': task.isDisabled, - } ); - - const taskItemProps = { - completed: task.isComplete, - onSnooze: task.isSnoozeable && onSnooze, - onDismiss: task.isDismissable && onDismiss, - }; - - const DefaultTaskItem = useCallback( - ( props ) => { - const onClickActions = () => { - if ( props.onClick ) { - trackClick(); - return props.onClick(); - } - return onTaskSelected(); - }; - return ( - {} } - actionLabel={ task.actionLabel } - { ...props } - onClick={ ( e: React.ChangeEvent ) => { - if ( task.isDisabled || e.target.tagName === 'A' ) { - return; - } - onClickActions(); - } } - /> - ); - }, - [ - task.id, - task.title, - task.content, - task.time, - task.actionLabel, - task.isComplete, - ] - ); - - return hasFills ? ( - - ) : ( - - ); -}; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx b/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx index 1f7b5d69d7a..48a3366a71a 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx @@ -259,56 +259,4 @@ describe( 'TaskList', () => { queryByText( dismissedTask[ 0 ].title ) ).not.toBeInTheDocument(); } ); - - it( 'should not display isSnoozed tasks', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() + 10000, - }, - ]; - const { queryByText } = render( - - ); - expect( - queryByText( dismissedTask[ 0 ].title ) - ).not.toBeInTheDocument(); - } ); - - it( 'should display a snoozed task if snoozedUntil passed the current timestamp', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() - 1000, - }, - ]; - const { queryByText } = render( - - ); - expect( queryByText( dismissedTask[ 0 ].title ) ).toBeInTheDocument(); - } ); } ); diff --git a/plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods b/plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods new file mode 100644 index 00000000000..245dc1feca4 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Cleanup and deprecate unused Task properties and methods diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Init.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Init.php index b2be20cff7f..83eba2630f4 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Init.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Init.php @@ -5,13 +5,7 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks; -use \Automattic\WooCommerce\Internal\Admin\Loader; -use Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore as TaxDataStore; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedOptions; -use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Appearance; -use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Products; -use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Tax; -use Automattic\WooCommerce\Admin\PluginsHelper; /** * Contains the logic for completing onboarding tasks. diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php index 6285b7f5d6b..03944a04fe4 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php @@ -27,6 +27,8 @@ abstract class Task { * Name of the snooze option. * * @var string + * + * @deprecated 7.2.0 */ const SNOOZED_OPTION = 'woocommerce_task_list_remind_me_later_tasks'; @@ -178,9 +180,13 @@ abstract class Task { /** * Level. * + * @deprecated 7.2.0 + * * @return string */ public function get_level() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + return 3; } @@ -267,18 +273,26 @@ abstract class Task { /** * Check if a task is snoozeable. * + * @deprecated 7.2.0 + * * @return bool */ public function is_snoozeable() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + return false; } /** * Get the snoozed until datetime. * + * @deprecated 7.2.0 + * * @return string */ public function get_snoozed_until() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + $snoozed_tasks = get_option( self::SNOOZED_OPTION, array() ); if ( isset( $snoozed_tasks[ $this->get_id() ] ) ) { return $snoozed_tasks[ $this->get_id() ]; @@ -290,9 +304,13 @@ abstract class Task { /** * Bool for task snoozed. * + * @deprecated 7.2.0 + * * @return bool */ public function is_snoozed() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + if ( ! $this->is_snoozeable() ) { return false; } @@ -306,9 +324,14 @@ abstract class Task { * Snooze the task. * * @param string $duration Duration to snooze. day|hour|week. + * + * @deprecated 7.2.0 + * * @return bool */ public function snooze( $duration = 'day' ) { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + if ( ! $this->is_snoozeable() ) { return false; } @@ -330,9 +353,13 @@ abstract class Task { /** * Undo task snooze. * + * @deprecated 7.2.0 + * * @return bool */ public function undo_snooze() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + $snoozed = get_option( self::SNOOZED_OPTION, array() ); unset( $snoozed[ $this->get_id() ] ); $update = update_option( self::SNOOZED_OPTION, $snoozed ); @@ -406,9 +433,13 @@ abstract class Task { /** * Check if task is disabled. * + * @deprecated 7.2.0 + * * @return bool */ public function is_disabled() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + return false; } @@ -453,15 +484,15 @@ abstract class Task { 'actionUrl' => $this->get_action_url(), 'isComplete' => $this->is_complete(), 'time' => $this->get_time(), - 'level' => $this->get_level(), + 'level' => 3, 'isActioned' => $this->is_actioned(), 'isDismissed' => $this->is_dismissed(), 'isDismissable' => $this->is_dismissable(), - 'isSnoozed' => $this->is_snoozed(), - 'isSnoozeable' => $this->is_snoozeable(), + 'isSnoozed' => false, + 'isSnoozeable' => false, 'isVisited' => $this->is_visited(), - 'isDisabled' => $this->is_disabled(), - 'snoozedUntil' => $this->get_snoozed_until(), + 'isDisabled' => false, + 'snoozedUntil' => null, 'additionalData' => self::convert_object_to_camelcase( $this->get_additional_data() ), 'eventPrefix' => $this->prefix_event( '' ), ); diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index 2ea980c9106..b769263d19b 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -99,6 +99,8 @@ class TaskList { /** * Array of TaskListSection. * + * @deprecated 7.2.0 + * * @var array */ private $sections = array(); @@ -106,6 +108,8 @@ class TaskList { /** * Key value map of task class and id used for sections. * + * @deprecated 7.2.0 + * * @var array */ public $task_class_id_map = array(); @@ -126,7 +130,6 @@ class TaskList { 'options' => array(), 'visible' => true, 'display_progress_header' => false, - 'sections' => array(), ); $data = wp_parse_args( $data, $defaults ); @@ -147,12 +150,6 @@ class TaskList { } $this->possibly_remove_reminder_bar(); - $this->sections = array_map( - function( $section ) { - return new TaskListSection( $section, $this ); - }, - $data['sections'] - ); } /** @@ -274,9 +271,7 @@ class TaskList { return; } - $task_class_name = substr( get_class( $task ), strrpos( get_class( $task ), '\\' ) + 1 ); - $this->task_class_id_map[ $task_class_name ] = $task->get_id(); - $this->tasks[] = $task; + $this->tasks[] = $task; } /** @@ -315,9 +310,13 @@ class TaskList { /** * Get task list sections. * + * @deprecated 7.2.0 + * * @return array */ public function get_sections() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + return $this->sections; } @@ -420,12 +419,6 @@ class TaskList { 'eventPrefix' => $this->prefix_event( '' ), 'displayProgressHeader' => $this->display_progress_header, 'keepCompletedTaskList' => $this->get_keep_completed_task_list(), - 'sections' => array_map( - function( $section ) { - return $section->get_json(); - }, - $this->sections - ), ); } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php index bc343781dd3..65c5f001163 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php @@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks; /** * Task List section class. + * + * @deprecated 7.2.0 */ class TaskListSection { diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php index e7ba5cdc3e7..cadd67a6d5b 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php @@ -39,9 +39,6 @@ class Appearance extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Make your store stand out with unique design', 'woocommerce' ); - } if ( $this->get_parent_option( 'use_completed_title' ) === true ) { if ( $this->is_complete() ) { return __( 'You personalized your store', 'woocommerce' ); @@ -57,9 +54,6 @@ class Appearance extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Upload your logo to adapt the store to your brand’s personality.', 'woocommerce' ); - } return __( 'Add your logo, create a homepage, and start designing your store.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php index 9a35e05b5d5..1ec834bf758 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php @@ -25,9 +25,6 @@ class Marketing extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Grow your business with marketing tools', 'woocommerce' ); - } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added sales channels', 'woocommerce' ); @@ -43,9 +40,6 @@ class Marketing extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Promote your store in other sales channels, like email, Google, and Facebook.', 'woocommerce' ); - } return __( 'Add recommended marketing tools to reach new customers and grow your business', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php index 5570ed8dc2e..d12f0846cda 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php @@ -3,7 +3,6 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks; use Automattic\WooCommerce\Admin\Features\Features; -use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; /** @@ -32,9 +31,6 @@ class Payments extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Add a way to get paid', 'woocommerce' ); - } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You set up payments', 'woocommerce' ); @@ -50,9 +46,6 @@ class Payments extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Let your customers pay the way they like.', 'woocommerce' ); - } return __( 'Choose payment providers and enable payment methods at checkout.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php index bae2bafda6f..0b2ee58fbd3 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php @@ -37,9 +37,6 @@ class Products extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Create or upload your first products', 'woocommerce' ); - } if ( $this->get_parent_option( 'use_completed_title' ) === true ) { if ( $this->is_complete() ) { return __( 'You added products', 'woocommerce' ); @@ -55,9 +52,6 @@ class Products extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Add products to sell and build your catalog.', 'woocommerce' ); - } return __( 'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php index 7fe8607b68b..d42d2322c26 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php @@ -43,9 +43,6 @@ class Shipping extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Select how to ship your products', 'woocommerce' ); - } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added shipping costs', 'woocommerce' ); @@ -61,9 +58,6 @@ class Shipping extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Set delivery costs and enable extra features, like shipping label printing.', 'woocommerce' ); - } return __( "Set your store location and where you'll ship to.", 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index ca508bd880e..57d52d2d726 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -62,9 +62,6 @@ class Tax extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Get taxes out of your mind', 'woocommerce' ); - } if ( $this->get_parent_option( 'use_completed_title' ) === true ) { if ( $this->is_complete() ) { return __( 'You added tax rates', 'woocommerce' ); @@ -80,9 +77,6 @@ class Tax extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Have sales tax calculated automatically, or add the rates manually.', 'woocommerce' ); - } return self::can_use_automated_taxes() ? __( 'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.', diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php index ad35e8b3103..495ae081525 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php @@ -51,7 +51,6 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case { // Resetting task list options and lists. update_option( Task::DISMISSED_OPTION, array() ); - update_option( Task::SNOOZED_OPTION, array() ); TaskLists::clear_lists(); } @@ -185,185 +184,6 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case { $this->assertSame( 'Custom post content', get_the_content( null, null, $data['post_id'] ) ); } - - /** - * Test that a task can be snoozed. - * @group tasklist - */ - public function test_task_can_be_snoozed() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task = TaskLists::get_task( 'test-task' ); - - $this->assertEquals( $data['isSnoozed'], true ); - $this->assertEquals( isset( $data['snoozedUntil'] ), true ); - $this->assertEquals( $task->is_snoozed(), true ); - $this->assertNotNull( $task->get_snoozed_until() ); - - } - - /** - * Test that a task can be snoozed with determined list ID. - * @group tasklist - */ - public function test_task_can_be_snoozed_with_list_id() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $request->set_body( wp_json_encode( array( 'task_list_id' => 'test-list' ) ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task = TaskLists::get_task( 'test-task' ); - - $this->assertEquals( $data['isSnoozed'], true ); - $this->assertEquals( isset( $data['snoozedUntil'] ), true ); - $this->assertEquals( $task->is_snoozed(), true ); - $this->assertNotNull( $task->get_snoozed_until() ); - } - - /** - * Test that a task can be snoozed with determined duration. - * @group tasklist - */ - public function test_task_can_be_snoozed_with_duration() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $request->set_body( wp_json_encode( array( 'duration' => 'week' ) ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task = TaskLists::get_task( 'test-task' ); - - $week_in_ms = WEEK_IN_SECONDS * 1000; - // Taking off 1 minute as matching a week is very precise and we might run into some race conditions otherwise. - $week_in_ms -= MINUTE_IN_SECONDS * 1000; - - $this->assertEquals( $data['snoozedUntil'] >= ( ( time() * 1000 ) + $week_in_ms ), true ); - - } - - /** - * Test that a snoozed task can be undone. - * @group tasklist - */ - public function test_snoozed_task_can_be_undone() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $task = TaskLists::get_task( 'test-task' ); - - $task->snooze(); - - $this->assertEquals( $task->is_snoozed(), true ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/undo_snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task_after_request = TaskLists::get_task( 'test-task' ); - - $this->assertEquals( $task_after_request->is_snoozed(), false ); - - } - - /** - * Test that snooze endpoint returns error for invalid task. - * @group tasklist - */ - public function test_snoozed_task_invalid() { - $this->markTestSkipped( 'Skipped temporarily due to change in endpoint behavior.' ); - wp_set_current_user( $this->user ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $response = $this->server->dispatch( $request ); - $response_data = $response->get_data(); - - $this->assertEquals( $response_data['data']['status'], 404 ); - $this->assertEquals( $response_data['code'], 'woocommerce_rest_invalid_task' ); - } - /** * Test that a task can be dismissed. * @group tasklist diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php index ba2ee818c71..bebd752a96f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php @@ -99,100 +99,6 @@ class WC_Admin_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case { } - /** - * Tests that a task can be snoozed. - */ - public function test_snooze() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => true, - ) - ); - - $update = $task->snooze(); - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $this->assertEquals( true, $update ); - $this->assertArrayHasKey( $task->get_id(), $snoozed ); - } - - /** - * Tests that a task can be unsnoozed. - */ - public function test_undo_snooze() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => true, - ) - ); - - $task->snooze(); - $task->undo_snooze(); - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $this->assertArrayNotHasKey( $task->get_id(), $snoozed ); - } - - /** - * Tests that a task's snooze time is automatically added. - */ - public function test_snoozed_until() { - $time = time() * 1000; - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $snoozed['wc-unit-test-task'] = $time; - update_option( Task::SNOOZED_OPTION, $snoozed ); - - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-task', - 'is_snoozeable' => true, - ) - ); - - $this->assertEquals( $time, $task->get_snoozed_until() ); - - } - - /** - * Tests that a non snoozeable task cannot be snoozed. - */ - public function test_not_snoozeable() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => false, - ) - ); - - $task->snooze(); - $this->assertEquals( false, $task->is_snoozed() ); - } - - /** - * Tests that a task is no longer consider snoozed after the time has passed. - */ - public function test_snooze_time() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => true, - ) - ); - - $time = time() * 1000 - 1; - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $snoozed['wc-unit-test-snoozeable-task'] = $time; - update_option( Task::SNOOZED_OPTION, $snoozed ); - - $this->assertEquals( false, $task->is_snoozed() ); - } - - /** * Tests that a task's properties are returned as JSON. */ From c561d7941dd5a369aceffc26fec63a253b539797 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 3 Nov 2022 11:08:14 +0100 Subject: [PATCH 130/149] Fix wrong return type get shipping tax (#35453) Was documented as returning array, now it's documented as returning float (and the ourput is converted to float). --- .../changelog/fix-wrong-return-type-get-shipping-tax | 4 ++++ plugins/woocommerce/includes/class-wc-shipping-rate.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax diff --git a/plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax b/plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax new file mode 100644 index 00000000000..bdc41c8d4e7 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix inconsistent return type of class WC_Shipping_Rate->get_shipping_tax() diff --git a/plugins/woocommerce/includes/class-wc-shipping-rate.php b/plugins/woocommerce/includes/class-wc-shipping-rate.php index f7eedbf3045..a42893ef5a1 100644 --- a/plugins/woocommerce/includes/class-wc-shipping-rate.php +++ b/plugins/woocommerce/includes/class-wc-shipping-rate.php @@ -226,10 +226,10 @@ class WC_Shipping_Rate { /** * Get shipping tax. * - * @return array + * @return float */ public function get_shipping_tax() { - return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); + return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? (float) array_sum( $this->taxes ) : 0.0, $this ); } /** From 925432aebee6edc3fdf926023559feb5d13e302b Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Thu, 3 Nov 2022 03:29:15 -0700 Subject: [PATCH 131/149] Admin redirects for HPOS URLs (#35463) * Redirect COT/HPOS admin requests to the corresponding CPT screen, if COT is not authoritative. * Tidy handling of query parameters. * Linting fixes. --- .../fix-35074-hpos-cpt-admin-redirects | 4 + .../includes/admin/class-wc-admin-menus.php | 3 + .../Admin/Orders/COTRedirectionController.php | 75 ++++++++ .../OrderAdminServiceProvider.php | 3 + .../Orders/COTRedirectionControllerTest.php | 181 ++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects create mode 100644 plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php create mode 100644 plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php diff --git a/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects b/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects new file mode 100644 index 00000000000..9a4fa61de72 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +When custom order tables are not authoritative, admin UI requests will be redirected to the matching legacy order screen as appropriate. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php index 191d65ca586..8b1491042b7 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -6,6 +6,7 @@ * @version 2.5.0 */ +use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\PageController as Custom_Orders_PageController; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Admin\Features\Features; @@ -316,6 +317,8 @@ class WC_Admin_Menus { if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { $this->orders_page_controller = new Custom_Orders_PageController(); $this->orders_page_controller->setup(); + } else { + wc_get_container()->get( COTRedirectionController::class )->setup(); } } diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php b/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php new file mode 100644 index 00000000000..f307bad74d7 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php @@ -0,0 +1,75 @@ +get( COTRedirectionController::class ), 'handle_hpos_admin_requests' ) + * ); + */ +class COTRedirectionController { + use AccessiblePrivateMethods; + + /** + * Add hooks needed to perform our magic. + */ + public function setup(): void { + // Only take action in cases where access to the admin screen would otherwise be denied. + self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) ); + } + + /** + * Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially + * redirect the user to the equivalent CPT-driven screens. + * + * @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used. + */ + private function handle_hpos_admin_requests( $query_params = null ) { + $query_params = is_array( $query_params ) ? $query_params : $_GET; + + if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) { + return; + } + + $params = wp_unslash( $query_params ); + $action = $params['action'] ?? ''; + unset( $params['page'] ); + + if ( 'edit' === $action && isset( $params['id'] ) ) { + $params['post'] = $params['id']; + unset( $params['id'] ); + $new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) ); + } elseif ( 'new' === $action ) { + unset( $params['action'] ); + $params['post_type'] = 'shop_order'; + $new_url = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) ); + } else { + // If nonce parameters are present and valid, rebuild them for the CPT admin list table. + if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) { + $params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' ); + $params['_wpnonce'] = wp_create_nonce( 'bulk-posts' ); + } + + // If an `order` array parameter is present, rename as `post`. + if ( isset( $params['order'] ) && is_array( $params['order'] ) ) { + $params['post'] = $params['order']; + unset( $params['order'] ); + } + + $params['post_type'] = 'shop_order'; + $new_url = add_query_arg( $params, get_admin_url( null, 'edit.php' ) ); + } + + if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) { + exit; + } + } +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php index ee473150be7..bbdd2a83d43 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; +use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\Edit; use Automattic\WooCommerce\Internal\Admin\Orders\ListTable; use Automattic\WooCommerce\Internal\Admin\Orders\PageController; @@ -21,6 +22,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { * @var string[] */ protected $provides = array( + COTRedirectionController::class, PageController::class, Edit::class, ListTable::class, @@ -32,6 +34,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { * @return void */ public function register() { + $this->share( COTRedirectionController::class ); $this->share( PageController::class ); $this->share( Edit::class )->addArgument( PageController::class ); $this->share( ListTable::class )->addArgument( PageController::class ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php new file mode 100644 index 00000000000..f15a085ff47 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php @@ -0,0 +1,181 @@ +sut = new COTRedirectionController(); + $this->sut->setup(); + $this->redirected_to = ''; + + add_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) ); + } + + /** + * Remove our redirect listener. + * + * @return void + */ + public function tearDown(): void { + parent::tearDown(); + remove_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) ); + } + + /** + * Captures the attempted redirect location, and stops the redirect from taking place. + * + * @param string $url Redirect location. + * + * @return null + */ + public function watch_and_anull_redirects( string $url ) { + $this->redirected_to = $url; + return null; + } + + /** + * Supplies the URL of the last attempted redirect, then resets ready for the next test. + * + * @return string + */ + private function get_redirect_attempt(): string { + $return = $this->redirected_to; + $this->redirected_to = ''; + return $return; + } + + /** + * Test that redirects only occur in relation to HPOS admin screen requests. + * + * @return void + */ + public function test_redirects_only_impact_hpos_admin_requests() { + $this->sut->handle_hpos_admin_requests( array( 'page' => 'wc-orders' ) ); + $this->assertNotEmpty( $this->get_redirect_attempt(), 'A redirect was attempted in relation to an HPOS admin request.' ); + + $this->sut->handle_hpos_admin_requests( array( 'page' => 'foo' ) ); + $this->assertEmpty( $this->get_redirect_attempt(), 'A redirect was not attempted in relation to a non-HPOS admin request.' ); + } + + /** + * Test order editor redirects work (in relation to creating new orders). + * + * @return void + */ + public function test_redirects_to_the_new_order_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'action' => 'new', + 'page' => 'wc-orders', + ) + ); + + $this->assertStringContainsString( + '/wp-admin/post-new.php?post_type=shop_order', + $this->get_redirect_attempt(), + 'Attempts to access the new order page (HPOS) are successfully redirected to the new order page (CPT).' + ); + } + + /** + * Test order editor redirects work (in relation to existing orders). + * + * @return void + */ + public function test_redirects_to_the_order_editor_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'action' => 'edit', + 'id' => 12345, + 'page' => 'wc-orders', + ) + ); + + $redirect_url = $this->get_redirect_attempt(); + $redirect_base = wp_parse_url( $redirect_url, PHP_URL_PATH ); + parse_str( wp_parse_url( $redirect_url, PHP_URL_QUERY ), $redirect_query ); + + $this->assertStringContainsString( + '/post.php', + $redirect_base, + 'Confirm order editor redirects go to the expected WordPress admin controller.' + ); + + $this->assertEquals( + '12345', + $redirect_query['post'], + 'Confirm order editor redirects maintain the correct order ID.' + ); + } + + /** + * Tests order list table redirects work. + * + * @return void + */ + public function test_redirects_to_the_order_admin_list_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'arbitrary' => '3pd-integration', + 'order' => array( + 123, + 456, + ), + 'page' => 'wc-orders', + ) + ); + + $redirect_url = $this->get_redirect_attempt(); + $redirect_base = wp_parse_url( $redirect_url, PHP_URL_PATH ); + parse_str( wp_parse_url( $redirect_url, PHP_URL_QUERY ), $redirect_query ); + + $this->assertStringContainsString( + '/edit.php', + $redirect_base, + 'Confirm order list table redirects go to the expected WordPress admin controller.' + ); + + $this->assertEquals( + array( + '123', + '456', + ), + $redirect_query['post'], + 'Confirm order list table redirects maintain a list of order IDs for bulk action requests (if one was passed).' + ); + + $this->assertEquals( + 'shop_order', + $redirect_query['post_type'], + 'Confirm order list table redirects reference the correct custom post type.' + ); + + $this->assertEquals( + '3pd-integration', + $redirect_query['arbitrary'], + 'Confirm that arbitrary query parameters are also passed across via order list table redirects.' + ); + } +} From 8ef8a380e5bea35ade8eb7302818e3945d45d914 Mon Sep 17 00:00:00 2001 From: Basti <51531217+crftwrk@users.noreply.github.com> Date: Thu, 3 Nov 2022 12:25:29 +0100 Subject: [PATCH 132/149] Fix version typo in form-login.php (#35479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Néstor Soriano --- plugins/woocommerce/changelog/fix-version-typo-form-login | 4 ++++ plugins/woocommerce/templates/global/form-login.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-version-typo-form-login diff --git a/plugins/woocommerce/changelog/fix-version-typo-form-login b/plugins/woocommerce/changelog/fix-version-typo-form-login new file mode 100644 index 00000000000..04268cb2b3b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-version-typo-form-login @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix @version header in form-login.php diff --git a/plugins/woocommerce/templates/global/form-login.php b/plugins/woocommerce/templates/global/form-login.php index 767d13ea60a..33776e45611 100644 --- a/plugins/woocommerce/templates/global/form-login.php +++ b/plugins/woocommerce/templates/global/form-login.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 7.1.0 + * @version 7.0.1 */ if ( ! defined( 'ABSPATH' ) ) { From dcafc4b5a5969b4643aacbf29a699fd7aaedc804 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 3 Nov 2022 18:12:50 +0530 Subject: [PATCH 133/149] Display correct order pending sync count on feature enabled screen. (#35480) * Display correct order pending sync count on feature enabled screen. * Add changelog. --- plugins/woocommerce/changelog/fix-percentage_type | 5 +++++ .../src/Internal/DataStores/Orders/DataSynchronizer.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-percentage_type diff --git a/plugins/woocommerce/changelog/fix-percentage_type b/plugins/woocommerce/changelog/fix-percentage_type new file mode 100644 index 00000000000..8b6580f80c0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-percentage_type @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Minor type fix. + + diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index b4849201ae2..2d30539c601 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -472,7 +472,7 @@ WHERE $extra_tip = sprintf( _n( "⚠ There's one order pending sync from the posts table to the orders table. The feature shouldn't be disabled until this order is synchronized.", - "⚠ There are %%1\$d orders pending sync from the posts table to the orders table. The feature shouldn't be disabled until these orders are synchronized.", + "⚠ There are %1\$d orders pending sync from the posts table to the orders table. The feature shouldn't be disabled until these orders are synchronized.", $pending_sync_count, 'woocommerce' ), From 1b0d8c077cfd13e1d41702fd8fd33b2f38bae44c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Nov 2022 09:25:02 -0500 Subject: [PATCH 134/149] Delete changelog files based on PR 35480 (#35484) Delete changelog files for 35480 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-percentage_type | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-percentage_type diff --git a/plugins/woocommerce/changelog/fix-percentage_type b/plugins/woocommerce/changelog/fix-percentage_type deleted file mode 100644 index 8b6580f80c0..00000000000 --- a/plugins/woocommerce/changelog/fix-percentage_type +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Minor type fix. - - From 5b1296fe45927cc03b2d286a64e5532afc6640e0 Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Thu, 3 Nov 2022 08:20:29 -0700 Subject: [PATCH 135/149] Adding attribute edit modal for products MVP (#35269) --- .../changelog/add-34332-add-attribute-edit | 4 + packages/js/components/package.json | 2 +- .../attribute-field/add-attribute-modal.tsx | 75 ++-- .../attribute-field/attribute-field.tsx | 157 ++++++- .../attribute-field/edit-attribute-modal.scss | 35 ++ .../attribute-field/edit-attribute-modal.tsx | 184 ++++++++ .../test/add-attribute-modal.spec.tsx | 22 +- .../test/attribute-field.spec.tsx | 255 ++++++----- .../products/fields/attribute-field/utils.ts | 4 +- .../attribute-input-field.tsx | 35 +- .../test/attribute-input-field.spec.tsx | 8 +- .../attribute-term-input-field.scss | 5 + .../attribute-term-input-field.tsx | 16 +- .../custom-attribute-term-input-field.tsx | 182 ++++++++ .../attribute-term-input-field/index.ts | 1 + .../products/sections/attributes-section.tsx | 9 +- .../changelog/add-34332-add-attribute-edit | 4 + pnpm-lock.yaml | 403 +++++++++--------- 18 files changed, 1027 insertions(+), 374 deletions(-) create mode 100644 packages/js/components/changelog/add-34332-add-attribute-edit create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx create mode 100644 plugins/woocommerce/changelog/add-34332-add-attribute-edit diff --git a/packages/js/components/changelog/add-34332-add-attribute-edit b/packages/js/components/changelog/add-34332-add-attribute-edit new file mode 100644 index 00000000000..42678d69bf9 --- /dev/null +++ b/packages/js/components/changelog/add-34332-add-attribute-edit @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Updating downshift to 6.1.12. diff --git a/packages/js/components/package.json b/packages/js/components/package.json index 3955a228b76..2a07a218727 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -74,7 +74,7 @@ "d3-shape": "^1.3.7", "d3-time-format": "^2.3.0", "dompurify": "^2.3.6", - "downshift": "^6.1.9", + "downshift": "^6.1.12", "emoji-flags": "^1.3.0", "gridicons": "^3.4.0", "memoize-one": "^6.0.0", diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx index effe7a7ed37..20bb4c5ab23 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx @@ -4,11 +4,11 @@ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { trash } from '@wordpress/icons'; -import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; import { Form, __experimentalSelectControlMenuSlot as SelectControlMenuSlot, } from '@woocommerce/components'; + import { Button, Modal, @@ -23,21 +23,19 @@ import { import './add-attribute-modal.scss'; import { AttributeInputField } from '../attribute-input-field'; import { AttributeTermInputField } from '../attribute-term-input-field'; +import { HydratedAttributeType } from '../attribute-field'; -type CreateCategoryModalProps = { +type AddAttributeModalProps = { onCancel: () => void; - onAdd: ( newCategories: ProductAttribute[] ) => void; + onAdd: ( newCategories: HydratedAttributeType[] ) => void; selectedAttributeIds?: number[]; }; type AttributeForm = { - attributes: { - attribute?: ProductAttribute; - terms: ProductAttributeTerm[]; - }[]; + attributes: Array< HydratedAttributeType | { id: undefined; terms: [] } >; }; -export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { +export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( { onCancel, onAdd, selectedAttributeIds = [], @@ -53,23 +51,18 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { setValue( 'attributes', [ ...values.attributes, { - attribute: undefined, + id: undefined, terms: [], }, ] ); }; const onAddingAttributes = ( values: AttributeForm ) => { - const newAttributesToAdd: ProductAttribute[] = []; + const newAttributesToAdd: HydratedAttributeType[] = []; values.attributes.forEach( ( attr ) => { - if ( - attr.attribute && - attr.attribute.name && - attr.terms.length > 0 - ) { + if ( attr.id && attr.name && ( attr.terms || [] ).length > 0 ) { newAttributesToAdd.push( { - ...( attr.attribute as ProductAttribute ), - options: attr.terms.map( ( term ) => term.name ), + ...( attr as HydratedAttributeType ), } ); } } ); @@ -91,7 +84,7 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { ); } else { setValue( `attributes[${ index }]`, [ - { attribute: undefined, terms: [] }, + { id: undefined, terms: [] }, ] ); } }; @@ -111,7 +104,7 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { const onClose = ( values: AttributeForm ) => { const hasValuesSet = values.attributes.some( - ( value ) => value?.attribute?.id && value?.terms?.length > 0 + ( value ) => value?.id && value?.terms && value?.terms.length > 0 ); if ( hasValuesSet ) { setShowConfirmClose( true ); @@ -124,7 +117,7 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { <> initialValues={ { - attributes: [ { attribute: undefined, terms: [] } ], + attributes: [ { id: undefined, terms: [] } ], } } > { ( { @@ -169,7 +162,7 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { { values.attributes.map( - ( { attribute, terms }, index ) => ( + ( formAttr, index ) => ( = ( { 'Search or create attribute', 'woocommerce' ) } - value={ attribute } + value={ + formAttr.id && + formAttr.name + ? formAttr + : null + } onChange={ ( val ) => { setValue( 'attributes[' + index + - '].attribute', - val + ']', + { + ...val, + terms: [], + options: + undefined, + } ); if ( val ) { focusValueField( @@ -196,22 +199,20 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { ); } } } - filteredAttributeIds={ [ + ignoredAttributeIds={ [ ...selectedAttributeIds, ...values.attributes .map( ( attr ) => - attr - ?.attribute - ?.id + attr?.id ) .filter( ( - id - ): id is number => - id !== + attrId + ): attrId is number => + attrId !== undefined ), ] } @@ -224,12 +225,14 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { 'woocommerce' ) } disabled={ - ! attribute?.id + ! formAttr.id } attributeId={ - attribute?.id + formAttr.id + } + value={ + formAttr.terms } - value={ terms } onChange={ ( val ) => @@ -252,7 +255,6 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { 1 && ! values .attributes[ 0 ] - ?.attribute ?.id } label={ __( @@ -306,8 +308,7 @@ export const AddAttributeModal: React.FC< CreateCategoryModalProps > = ( { ) } disabled={ values.attributes.length === 1 && - ! values.attributes[ 0 ]?.attribute - ?.id && + ! values.attributes[ 0 ]?.id && values.attributes[ 0 ]?.terms ?.length === 0 } diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx index 5f855199f2d..9fea1840def 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx @@ -3,10 +3,19 @@ */ import { sprintf, __ } from '@wordpress/i18n'; import { Button, Card, CardBody } from '@wordpress/components'; -import { useState } from '@wordpress/element'; -import { ProductAttribute } from '@woocommerce/data'; +import { useState, useCallback, useEffect } from '@wordpress/element'; +import { + ProductAttribute, + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, + ProductAttributeTerm, +} from '@woocommerce/data'; +import { resolveSelect } from '@wordpress/data'; import { Text } from '@woocommerce/experimental'; -import { Sortable, ListItem } from '@woocommerce/components'; +import { + Sortable, + ListItem, + __experimentalSelectControlMenuSlot as SelectControlMenuSlot, +} from '@woocommerce/components'; import { closeSmall } from '@wordpress/icons'; /** @@ -15,29 +24,120 @@ import { closeSmall } from '@wordpress/icons'; import './attribute-field.scss'; import AttributeEmptyStateLogo from './attribute-empty-state-logo.svg'; import { AddAttributeModal } from './add-attribute-modal'; +import { EditAttributeModal } from './edit-attribute-modal'; import { reorderSortableProductAttributePositions } from './utils'; +import { sift } from '../../../utils'; type AttributeFieldProps = { value: ProductAttribute[]; onChange: ( value: ProductAttribute[] ) => void; + productId?: number; +}; + +export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & { + options?: string[]; + terms?: ProductAttributeTerm[]; }; export const AttributeField: React.FC< AttributeFieldProps > = ( { value, onChange, + productId, } ) => { const [ showAddAttributeModal, setShowAddAttributeModal ] = useState( false ); + const [ hydrationComplete, setHydrationComplete ] = useState< boolean >( + value ? false : true + ); + const [ hydratedAttributes, setHydratedAttributes ] = useState< + HydratedAttributeType[] + >( [] ); + const [ editingAttributeId, setEditingAttributeId ] = useState< + null | string + >( null ); + + const fetchTerms = useCallback( + ( attributeId: number ) => { + return resolveSelect( + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME + ) + .getProductAttributeTerms< ProductAttributeTerm[] >( { + attribute_id: attributeId, + product: productId, + } ) + .then( + ( attributeTerms ) => { + return attributeTerms; + }, + ( error ) => { + return error; + } + ); + }, + [ productId ] + ); + + useEffect( () => { + if ( ! value || hydrationComplete ) { + return; + } + + const [ customAttributes, globalAttributes ]: ProductAttribute[][] = + sift( value, ( attr: ProductAttribute ) => attr.id === 0 ); + + Promise.all( + globalAttributes.map( ( attr ) => fetchTerms( attr.id ) ) + ).then( ( allResults ) => { + setHydratedAttributes( [ + ...globalAttributes.map( ( attr, index ) => { + const newAttr = { + ...attr, + terms: allResults[ index ], + options: undefined, + }; + + return newAttr; + } ), + ...customAttributes, + ] ); + setHydrationComplete( true ); + } ); + }, [ productId, value, hydrationComplete ] ); + + const fetchAttributeId = ( attribute: { id: number; name: string } ) => + `${ attribute.id }-${ attribute.name }`; + + const updateAttributes = ( attributes: HydratedAttributeType[] ) => { + setHydratedAttributes( attributes ); + onChange( + attributes.map( ( attr ) => { + return { + ...attr, + options: attr.terms + ? attr.terms.map( ( term ) => term.name ) + : ( attr.options as string[] ), + terms: undefined, + }; + } ) + ); + }; + const onRemove = ( attribute: ProductAttribute ) => { // eslint-disable-next-line no-alert if ( window.confirm( __( 'Remove this attribute?', 'woocommerce' ) ) ) { - onChange( value.filter( ( attr ) => attr.id !== attribute.id ) ); + updateAttributes( + hydratedAttributes.filter( + ( attr ) => + fetchAttributeId( attr ) !== + fetchAttributeId( attribute ) + ) + ); } }; - const onAddNewAttributes = ( newAttributes: ProductAttribute[] ) => { - onChange( [ - ...( value || [] ), + const onAddNewAttributes = ( newAttributes: HydratedAttributeType[] ) => { + updateAttributes( [ + ...( hydratedAttributes || [] ), ...newAttributes .filter( ( newAttr ) => @@ -53,7 +153,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { setShowAddAttributeModal( false ); }; - if ( ! value || value.length === 0 ) { + if ( ! value || value.length === 0 || hydratedAttributes.length === 0 ) { return ( @@ -111,6 +211,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { }, {} as Record< number, ProductAttribute > ); + return (
= ( { } } > { sortedAttributes.map( ( attribute ) => ( - +
{ attribute.name }
{ attribute.options @@ -147,7 +248,14 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { ) }
-
); }; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss new file mode 100644 index 00000000000..420debdc77a --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss @@ -0,0 +1,35 @@ +.woocommerce-edit-attribute-modal { + overflow: visible; +} + +.woocommerce-edit-attribute-modal__body { + width: 500px; + max-width: 100%; + + .woocommerce-experimental-select-control + .woocommerce-experimental-select-control { + margin-top: 1.3em; + } + + .woocommerce-experimental-select-control__label, + .components-base-control__label { + font-size: 14px; + color: #757575; + font-weight: bold; + text-transform: none; + } + + .woocommerce-edit-attribute-modal__option-container { + display: flex; + flex-direction: row; + align-items: center; + } + + .woocommerce-attribute-term-field { + margin-bottom: 1.5em; + } + + .woocommerce-edit-attribute-modal__helper-text { + color: #757575; + margin: 0.5em 0 1.5em 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx new file mode 100644 index 00000000000..19e82720c5e --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx @@ -0,0 +1,184 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + Button, + Modal, + CheckboxControl, + TextControl, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { + __experimentalTooltip as Tooltip, + Link, +} from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; +import { getAdminLink } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import { + AttributeTermInputField, + CustomAttributeTermInputField, +} from '../attribute-term-input-field'; +import { HydratedAttributeType } from './attribute-field'; + +import './edit-attribute-modal.scss'; + +type EditAttributeModalProps = { + onCancel: () => void; + onEdit: ( alteredAttribute: HydratedAttributeType ) => void; + attribute: HydratedAttributeType; +}; + +export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( { + onCancel, + onEdit, + attribute, +} ) => { + const [ editableAttribute, setEditableAttribute ] = useState< + HydratedAttributeType | undefined + >( { ...attribute } ); + + const isCustomAttribute = editableAttribute?.id === 0; + + return ( + onCancel() } + className="woocommerce-edit-attribute-modal" + > +
+ + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + name: val, + } ) + } + /> +

+ { ! isCustomAttribute + ? interpolateComponents( { + mixedString: __( + `You can change the attribute's name in {{link}}Attributes{{/link}}.`, + 'woocommerce' + ), + components: { + link: ( + + <> + + ), + }, + } ) + : __( + 'Your customers will see this on the product page', + 'woocommerce' + ) } +

+ { attribute.terms ? ( + { + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + terms: val, + } ); + } } + /> + ) : ( + { + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + options: val, + } ); + } } + /> + ) } + +
+ + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + visible: val, + } ) + } + checked={ editableAttribute?.visible } + label={ __( 'Visible to customers', 'woocommerce' ) } + /> + +
+
+ + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + variation: val, + } ) + } + checked={ editableAttribute?.variation } + label={ __( 'Used for filters', 'woocommerce' ) } + /> + +
+
+
+ + +
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx index c88173c4fe9..d91848deb76 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx @@ -259,7 +259,7 @@ describe( 'AddAttributeModal', () => { expect( onAddMock ).toHaveBeenCalledWith( [] ); } ); - it( 'should add attribute with terms as string of options', () => { + it( 'should add attribute with array of terms', () => { const onAddMock = jest.fn(); const { queryByRole } = render( { attributeTermList[ 1 ], ] ); queryByRole( 'button', { name: 'Add attributes' } )?.click(); - expect( onAddMock ).toHaveBeenCalledWith( [ - { - ...attributeList[ 0 ], - options: [ - attributeTermList[ 0 ].name, - attributeTermList[ 1 ].name, - ], - }, - ] ); + + const onAddMockCalls = onAddMock.mock.calls[ 0 ][ 0 ]; + + expect( onAddMockCalls ).toHaveLength( 1 ); + expect( onAddMockCalls[ 0 ].id ).toEqual( attributeList[ 0 ].id ); + expect( onAddMockCalls[ 0 ].terms[ 0 ].name ).toEqual( + attributeTermList[ 0 ].name + ); + expect( onAddMockCalls[ 0 ].terms[ 1 ].name ).toEqual( + attributeTermList[ 1 ].name + ); } ); } ); } ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx index d6152e19562..542a1fe7e07 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx @@ -1,48 +1,16 @@ /** * External dependencies */ -import { render } from '@testing-library/react'; +import { render, act, screen, waitFor } from '@testing-library/react'; import { useState, useEffect } from '@wordpress/element'; import { ProductAttribute } from '@woocommerce/data'; +import { resolveSelect } from '@wordpress/data'; /** * Internal dependencies */ import { AttributeField } from '../attribute-field'; -let triggerDrag: ( items: Array< { key: string } > ) => void; - -jest.mock( '@woocommerce/components', () => ( { - __esModule: true, - ListItem: ( { children }: { children: JSX.Element } ) => children, - Sortable: ( { - onOrderChange, - children, - }: { - onOrderChange: ( items: Array< { key: string } > ) => void; - children: JSX.Element[]; - } ) => { - const [ items, setItems ] = useState< JSX.Element[] >( [] ); - useEffect( () => { - if ( ! children ) { - return; - } - setItems( Array.isArray( children ) ? children : [ children ] ); - }, [ children ] ); - - triggerDrag = ( newItems: Array< { key: string } > ) => { - onOrderChange( newItems ); - }; - return ( - <> - { items.map( ( child, index ) => ( -
{ child }
- ) ) } - - ); - }, -} ) ); - const attributeList: ProductAttribute[] = [ { id: 15, @@ -75,6 +43,66 @@ const attributeList: ProductAttribute[] = [ }, ]; +let triggerDrag: ( items: Array< { key: string } > ) => void; + +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + resolveSelect: jest.fn().mockReturnValue( { + getProductAttributeTerms: ( { + attribute_id, + }: { + attribute_id: number; + } ) => + new Promise( ( resolve ) => { + const attr = attributeList.find( + ( item ) => item.id === attribute_id + ); + resolve( + attr?.options.map( ( itemName, index ) => ( { + id: ++index, + slug: itemName.toLowerCase(), + name: itemName, + description: '', + menu_order: ++index, + count: ++index, + } ) ) + ); + } ), + } ), +} ) ); + +jest.mock( '@woocommerce/components', () => ( { + __esModule: true, + ListItem: ( { children }: { children: JSX.Element } ) => children, + __experimentalSelectControlMenuSlot: () => null, + Sortable: ( { + onOrderChange, + children, + }: { + onOrderChange: ( items: Array< { key: string } > ) => void; + children: JSX.Element[]; + } ) => { + const [ items, setItems ] = useState< JSX.Element[] >( [] ); + useEffect( () => { + if ( ! children ) { + return; + } + setItems( Array.isArray( children ) ? children : [ children ] ); + }, [ children ] ); + + triggerDrag = ( newItems: Array< { key: string } > ) => { + onOrderChange( newItems ); + }; + return ( + <> + { items.map( ( child, index ) => ( +
{ child }
+ ) ) } + + ); + }, +} ) ); + describe( 'AttributeField', () => { beforeEach( () => { jest.clearAllMocks(); @@ -90,103 +118,138 @@ describe( 'AttributeField', () => { } ); } ); - it( 'should render the list of existing attributes', () => { - const { queryByText } = render( - {} } - /> - ); - expect( queryByText( 'No attributes yet' ) ).not.toBeInTheDocument(); - expect( queryByText( 'Add first attribute' ) ).not.toBeInTheDocument(); - expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); - expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); - } ); + it( 'should render the list of existing attributes', async () => { + act( () => { + render( + {} } + /> + ); + } ); - it( 'should render the first two terms of each attribute, and show "+ n more" for the rest', () => { - const { queryByText } = render( - {} } - /> - ); expect( - queryByText( attributeList[ 0 ].options[ 0 ] ) - ).toBeInTheDocument(); - expect( - queryByText( attributeList[ 1 ].options[ 0 ] ) - ).toBeInTheDocument(); - expect( - queryByText( attributeList[ 1 ].options[ 1 ] ) - ).toBeInTheDocument(); - expect( - queryByText( attributeList[ 1 ].options[ 2 ] ) + await screen.findByText( 'No attributes yet' ) ).not.toBeInTheDocument(); expect( - queryByText( + await screen.findByText( attributeList[ 0 ].name ) + ).toBeInTheDocument(); + expect( + await screen.findByText( attributeList[ 1 ].name ) + ).toBeInTheDocument(); + } ); + + it( 'should render the first two terms of each attribute, and show "+ n more" for the rest', async () => { + act( () => { + render( + {} } + /> + ); + } ); + + expect( + await screen.findByText( attributeList[ 0 ].options[ 0 ] ) + ).toBeInTheDocument(); + expect( + await screen.findByText( attributeList[ 1 ].options[ 0 ] ) + ).toBeInTheDocument(); + expect( + await screen.findByText( attributeList[ 1 ].options[ 1 ] ) + ).toBeInTheDocument(); + expect( + await screen.queryByText( attributeList[ 1 ].options[ 2 ] ) + ).not.toBeInTheDocument(); + expect( + await screen.queryByText( `+ ${ attributeList[ 1 ].options.length - 2 } more` ) ).not.toBeInTheDocument(); } ); describe( 'deleting', () => { - it( 'should show a window confirm when trash icon is clicked', () => { + it( 'should show a window confirm when trash icon is clicked', async () => { jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false ); - const { queryAllByLabelText } = render( - {} } - /> - ); - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + act( () => { + render( + {} } + /> + ); + } ); + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); expect( global.confirm ).toHaveBeenCalled(); } ); - it( 'should trigger onChange with removed item when user clicks ok on alert', () => { + it( 'should trigger onChange with removed item when user clicks ok on alert', async () => { jest.spyOn( global, 'confirm' ).mockReturnValueOnce( true ); const onChange = jest.fn(); - const { queryAllByLabelText } = render( - - ); - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + + act( () => { + render( + + ); + } ); + + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); + expect( global.confirm ).toHaveBeenCalled(); expect( onChange ).toHaveBeenCalledWith( [ attributeList[ 1 ] ] ); } ); - it( 'should not trigger onChange with removed item when user cancel', () => { + it( 'should not trigger onChange with removed item when user cancel', async () => { jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false ); const onChange = jest.fn(); - const { queryAllByLabelText } = render( - - ); - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + act( () => { + render( + + ); + } ); + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); expect( global.confirm ).toHaveBeenCalled(); expect( onChange ).not.toHaveBeenCalled(); } ); } ); describe( 'dragging', () => { - it( 'should trigger onChange with new order when onOrderChange triggered', () => { + it.skip( 'should trigger onChange with new order when onOrderChange triggered', async () => { + jest.spyOn( global, 'confirm' ).mockReturnValueOnce( true ); const onChange = jest.fn(); - const { queryAllByLabelText } = render( - - ); + + act( () => { + render( + + ); + } ); + if ( triggerDrag ) { triggerDrag( [ { key: attributeList[ 1 ].id.toString() }, { key: attributeList[ 0 ].id.toString() }, ] ); } - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); + expect( onChange ).toHaveBeenCalledWith( [ { ...attributeList[ 1 ], position: 0 }, { ...attributeList[ 0 ], position: 1 }, diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts b/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts index 2fc1aa6dfb3..5ff34d939e7 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts @@ -6,8 +6,8 @@ import { ProductAttribute } from '@woocommerce/data'; /** * Updates the position of a product attribute from the new items JSX.Element list. * - * @param { JSX.Element[] } items list of JSX elements coming back from sortable container. - * @param { Object } attributeKeyValues key value pair of product attributes. + * @param { JSX.Element[] } items list of JSX elements coming back from sortable container. + * @param { Object } attributeKeyValues key value pair of product attributes. */ export function reorderSortableProductAttributePositions( items: JSX.Element[], diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx index 3563992023b..68210642193 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx @@ -16,24 +16,26 @@ import { __experimentalSelectControlMenuItem as MenuItem, } from '@woocommerce/components'; +type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >; + type AttributeInputFieldProps = { - value?: ProductAttribute; + value?: Pick< QueryProductAttribute, 'id' | 'name' > | null; onChange: ( value?: Omit< ProductAttribute, 'position' | 'visible' | 'variation' > ) => void; label?: string; placeholder?: string; disabled?: boolean; - filteredAttributeIds?: number[]; + ignoredAttributeIds?: number[]; }; export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { - value, + value = null, onChange, placeholder, label, disabled, - filteredAttributeIds = [], + ignoredAttributeIds = [], } ) => { const { attributes, isLoading } = useSelect( ( select: WCDataSelector ) => { const { getProductAttributes, hasFinishedResolution } = select( @@ -46,26 +48,25 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { } ); const getFilteredItems = ( - allItems: Pick< QueryProductAttribute, 'id' | 'name' >[], + allItems: NarrowedQueryAttribute[], inputValue: string ) => { + const ignoreIdsFilter = ( item: NarrowedQueryAttribute ) => + ignoredAttributeIds.length + ? ! ignoredAttributeIds.includes( item.id ) + : true; + return allItems.filter( ( item ) => - filteredAttributeIds.indexOf( item.id ) < 0 && + ignoreIdsFilter( item ) && ( item.name || '' ) .toLowerCase() .startsWith( inputValue.toLowerCase() ) ); }; - const selected: Pick< QueryProductAttribute, 'id' | 'name' > | null = value - ? { - id: value.id, - name: value.name, - } - : null; return ( - > + items={ attributes || [] } label={ label || '' } disabled={ disabled } @@ -73,14 +74,14 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { placeholder={ placeholder } getItemLabel={ ( item ) => item?.name || '' } getItemValue={ ( item ) => item?.id || '' } - selected={ selected } - onSelect={ ( attribute ) => + selected={ value } + onSelect={ ( attribute ) => { onChange( { id: attribute.id, name: attribute.name, options: [], - } ) - } + } ); + } } onRemove={ () => onChange() } > { ( { diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx index 3b38ce81b45..0458350ae90 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx @@ -144,7 +144,7 @@ describe( 'AttributeInputField', () => { expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); } ); - it( 'should filter out attribute ids passed into filteredAttributeIds', () => { + it( 'should filter out attribute ids passed into ignoredAttributeIds', () => { ( useSelect as jest.Mock ).mockReturnValue( { isLoading: false, attributes: attributeList, @@ -152,7 +152,7 @@ describe( 'AttributeInputField', () => { const { queryByText } = render( ); expect( queryByText( 'spinner' ) ).not.toBeInTheDocument(); @@ -177,7 +177,7 @@ describe( 'AttributeInputField', () => { expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); } ); - it( 'should filter out attributes ids from filteredAttributeIds', () => { + it( 'should filter out attributes ids from ignoredAttributeIds', () => { ( useSelect as jest.Mock ).mockReturnValue( { isLoading: false, attributes: attributeList, @@ -185,7 +185,7 @@ describe( 'AttributeInputField', () => { const { queryByText } = render( ); expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss index a3509b2b7ef..1bc3b232ed4 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss @@ -11,3 +11,8 @@ margin-right: $gap-small; } } + +.woocommerce-attribute-term-field__add-new { + display: flex; + align-items: center; +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx index dfa6f30a6fc..32fdc11d0d0 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx @@ -30,13 +30,21 @@ type AttributeTermInputFieldProps = { attributeId?: number; placeholder?: string; disabled?: boolean; + label?: string; }; let uniqueId = 0; export const AttributeTermInputField: React.FC< AttributeTermInputFieldProps -> = ( { value = [], onChange, placeholder, disabled, attributeId } ) => { +> = ( { + value = [], + onChange, + placeholder, + disabled, + attributeId, + label = '', +} ) => { const attributeTermInputId = useRef( `woocommerce-attribute-term-field-${ ++uniqueId }` ); @@ -48,7 +56,7 @@ export const AttributeTermInputField: React.FC< useState< string >(); const fetchItems = useCallback( - ( searchString: string | undefined ) => { + ( searchString?: string | undefined ) => { setIsFetching( true ); return resolveSelect( EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME @@ -80,7 +88,7 @@ export const AttributeTermInputField: React.FC< attributeId !== undefined && ! fetchedItems.length ) { - fetchItems( '' ); + fetchItems(); } }, [ disabled, attributeId ] ); @@ -124,7 +132,7 @@ export const AttributeTermInputField: React.FC< items={ fetchedItems } multiple disabled={ disabled || ! attributeId } - label="" + label={ label } getFilteredItems={ ( allItems, inputValue ) => { if ( inputValue.length > 0 && diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx new file mode 100644 index 00000000000..8ca43d15916 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import { sprintf, __ } from '@wordpress/i18n'; +import { CheckboxControl, Icon } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { plus } from '@wordpress/icons'; +import { + __experimentalSelectControl as SelectControl, + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './attribute-term-input-field.scss'; + +type CustomAttributeTermInputFieldProps = { + value?: string[]; + onChange: ( value: string[] ) => void; + placeholder?: string; + label?: string; + disabled?: boolean; +}; + +type NewTermItem = { + id: string; + label: string; +}; + +function isNewTermItem( + item: NewTermItem | string | null +): item is NewTermItem { + return item !== null && typeof item === 'object' && !! item.label; +} + +export const CustomAttributeTermInputField: React.FC< + CustomAttributeTermInputFieldProps +> = ( { value = [], onChange, placeholder, disabled, label } ) => { + const [ listItems, setListItems ] = + useState< Array< string | NewTermItem > >( value ); + + const onRemove = ( item: string | NewTermItem ) => { + onChange( value.filter( ( opt ) => opt !== item ) ); + }; + + const onSelect = ( item: string | NewTermItem ) => { + // Add new item. + if ( isNewTermItem( item ) ) { + setListItems( [ ...listItems, item.label ] ); + onChange( [ ...value, item.label ] ); + return; + } + const isSelected = value.includes( item ); + if ( isSelected ) { + onRemove( item ); + return; + } + onChange( [ ...value, item ] ); + }; + + return ( + <> + + items={ listItems } + multiple + disabled={ disabled } + label={ label || '' } + placeholder={ placeholder || '' } + getItemLabel={ ( item ) => + isNewTermItem( item ) ? item.label : item || '' + } + getItemValue={ ( item ) => + isNewTermItem( item ) ? item.id : item || '' + } + getFilteredItems={ ( allItems, inputValue ) => { + const filteredItems = allItems.filter( + ( item ) => + ! inputValue.length || + ( ! isNewTermItem( item ) && + item + .toLowerCase() + .includes( inputValue.toLowerCase() ) ) + ); + if ( + inputValue.length > 0 && + ! filteredItems.find( + ( item ) => + ! isNewTermItem( item ) && + item.toLowerCase() === inputValue.toLowerCase() + ) + ) { + return [ + ...filteredItems, + { + id: 'is-new', + label: inputValue, + }, + ]; + } + return filteredItems; + } } + selected={ value } + onSelect={ onSelect } + onRemove={ onRemove } + className="woocommerce-attribute-term-field" + > + { ( { + items, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { items.map( ( item, menuIndex ) => { + return ( + + { isNewTermItem( item ) ? ( +
+ + + { sprintf( + /* translators: The name of the new attribute term to be created */ + __( + 'Create "%s"', + 'woocommerce' + ), + item.label + ) } + +
+ ) : ( + null } + checked={ value.includes( + item + ) } + label={ + + { item } + + } + /> + ) } +
+ ); + } ) } +
+ ); + } } + + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts index 562b73ccb93..f7a35f4a1e1 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts @@ -1 +1,2 @@ export * from './attribute-term-input-field'; +export * from './custom-attribute-term-input-field'; diff --git a/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx b/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx index ecd11453fd1..e2141df7e9c 100644 --- a/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx @@ -14,7 +14,10 @@ import { ProductSectionLayout } from '../layout/product-section-layout'; import { AttributeField } from '../fields/attribute-field'; export const AttributesSection: React.FC = () => { - const { getInputProps } = useFormContext< Product >(); + const { + getInputProps, + values: { id: productId }, + } = useFormContext< Product >(); return ( { } > - + ); }; diff --git a/plugins/woocommerce/changelog/add-34332-add-attribute-edit b/plugins/woocommerce/changelog/add-34332-add-attribute-edit new file mode 100644 index 00000000000..a2a242fb00e --- /dev/null +++ b/plugins/woocommerce/changelog/add-34332-add-attribute-edit @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding attribute edit modal for new product screen. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb65b4dd2c9..178406855ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: d3-shape: ^1.3.7 d3-time-format: ^2.3.0 dompurify: ^2.3.6 - downshift: ^6.1.9 + downshift: ^6.1.12 emoji-flags: ^1.3.0 eslint: ^8.12.0 gridicons: ^3.4.0 @@ -325,7 +325,7 @@ importers: d3-shape: 1.3.7 d3-time-format: 2.3.0 dompurify: 2.3.6 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 emoji-flags: 1.3.0 gridicons: 3.4.0_react@17.0.2 lodash: 4.17.21 @@ -2571,6 +2571,7 @@ packages: dependencies: '@babel/helper-explode-assignable-expression': 7.18.6 '@babel/types': 7.19.3 + dev: true /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} @@ -2878,6 +2879,24 @@ packages: - supports-color dev: true + /@babel/helper-define-polyfill-provider/0.3.0_@babel+core@7.16.12: + resolution: {integrity: sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/traverse': 7.19.3 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/helper-define-polyfill-provider/0.3.0_@babel+core@7.17.8: resolution: {integrity: sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==} peerDependencies: @@ -3142,7 +3161,6 @@ packages: /@babel/helper-plugin-utils/7.14.5: resolution: {integrity: sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-plugin-utils/7.18.9: resolution: {integrity: sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==} @@ -3280,7 +3298,6 @@ packages: /@babel/helper-validator-identifier/7.15.7: resolution: {integrity: sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier/7.16.7: resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} @@ -3373,7 +3390,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.16.12: @@ -3383,7 +3400,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.17.8: @@ -3473,7 +3490,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-remap-async-to-generator': 7.16.8 '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.12.9 transitivePeerDependencies: @@ -3487,7 +3504,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-remap-async-to-generator': 7.16.8 '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.16.12 transitivePeerDependencies: @@ -3542,8 +3559,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.18.9 transitivePeerDependencies: - supports-color dev: true @@ -3555,8 +3572,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.18.9 transitivePeerDependencies: - supports-color dev: false @@ -3593,7 +3610,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.12.9 transitivePeerDependencies: - supports-color @@ -3607,7 +3624,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -3672,7 +3689,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 dev: true @@ -3683,7 +3700,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.16.12 dev: false @@ -3736,7 +3753,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 dev: true @@ -3747,7 +3764,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.16.12 dev: false @@ -3790,7 +3807,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 dev: true @@ -3801,7 +3818,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.16.12 dev: false @@ -3844,7 +3861,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 dev: true @@ -3855,7 +3872,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.16.12 dev: false @@ -3951,7 +3968,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 dev: true @@ -3962,7 +3979,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.16.12 dev: false @@ -4085,7 +4102,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 dev: true @@ -4096,7 +4113,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.16.12 dev: false @@ -4276,7 +4293,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.12.9 transitivePeerDependencies: - supports-color @@ -4291,7 +4308,7 @@ packages: '@babel/core': 7.16.12 '@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -4345,7 +4362,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.16.12: @@ -4356,7 +4373,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.17.8: @@ -4476,7 +4493,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.16.12: @@ -4486,7 +4503,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.17.8: @@ -4496,7 +4513,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-decorators/7.16.0_@babel+core@7.17.8: resolution: {integrity: sha512-nxnnngZClvlY13nHJAIDow0S7Qzhq64fQ/NlqS+VER3kjW/4F0jLhXjeL8jcwSwz6Ca3rotT5NJD2T9I7lcv7g==} @@ -4929,7 +4946,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.16.12: @@ -4939,7 +4956,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.17.8: @@ -5047,7 +5064,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.16.12: @@ -5057,7 +5074,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.17.8: @@ -5096,7 +5113,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.16.12: @@ -5106,7 +5123,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.17.8: @@ -5157,7 +5174,7 @@ packages: '@babel/helper-environment-visitor': 7.16.7 '@babel/helper-function-name': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-replace-supers': 7.16.7 '@babel/helper-split-export-declaration': 7.16.7 globals: 11.12.0 @@ -5176,7 +5193,7 @@ packages: '@babel/helper-environment-visitor': 7.16.7 '@babel/helper-function-name': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-replace-supers': 7.16.7 '@babel/helper-split-export-declaration': 7.16.7 globals: 11.12.0 @@ -5239,7 +5256,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.16.12: @@ -5249,7 +5266,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.17.8: @@ -5288,7 +5305,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.16.12: @@ -5298,7 +5315,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.17.8: @@ -5413,7 +5430,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.16.12: @@ -5423,7 +5440,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.17.8: @@ -5463,7 +5480,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 '@babel/helper-plugin-utils': 7.19.0 dev: true @@ -5474,7 +5491,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 '@babel/helper-plugin-utils': 7.19.0 dev: false @@ -5526,7 +5543,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.16.12: @@ -5536,7 +5553,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.17.8: @@ -5578,7 +5595,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 '@babel/helper-function-name': 7.16.7 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.16.12: @@ -5590,7 +5607,7 @@ packages: '@babel/core': 7.16.12 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 '@babel/helper-function-name': 7.16.7 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.17.8: @@ -5633,7 +5650,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-literals/7.16.7_@babel+core@7.16.12: @@ -5643,7 +5660,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-literals/7.16.7_@babel+core@7.17.8: @@ -5682,7 +5699,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.16.12: @@ -5692,7 +5709,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.17.8: @@ -5806,7 +5823,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-simple-access': 7.17.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5821,7 +5838,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-simple-access': 7.17.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5881,7 +5898,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-hoist-variables': 7.16.7 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-identifier': 7.16.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5897,7 +5914,7 @@ packages: '@babel/core': 7.16.12 '@babel/helper-hoist-variables': 7.16.7 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-identifier': 7.16.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -6066,7 +6083,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.16.12: @@ -6076,7 +6093,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.17.8: @@ -6118,7 +6135,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-replace-supers': 7.16.7 transitivePeerDependencies: - supports-color @@ -6131,7 +6148,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-replace-supers': 7.16.7 transitivePeerDependencies: - supports-color @@ -6179,7 +6196,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.16.12: @@ -6189,7 +6206,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.17.8: @@ -6199,7 +6216,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.12.9: @@ -6248,7 +6265,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.16.12: @@ -6258,7 +6275,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.17.8: @@ -6350,9 +6367,9 @@ packages: '@babel/core': 7.16.12 '@babel/helper-annotate-as-pure': 7.16.0 '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.14.5 '@babel/plugin-syntax-jsx': 7.16.0_@babel+core@7.16.12 - '@babel/types': 7.19.3 + '@babel/types': 7.16.0 dev: false /@babel/plugin-transform-react-jsx/7.17.3_@babel+core@7.17.8: @@ -6460,7 +6477,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.16.12: @@ -6470,7 +6487,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.17.8: @@ -6537,7 +6554,10 @@ packages: '@babel/helper-plugin-utils': 7.19.0 babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 + babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.17.8 semver: 6.3.0 + transitivePeerDependencies: + - supports-color dev: true /@babel/plugin-transform-runtime/7.19.1_@babel+core@7.17.8: @@ -6573,7 +6593,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.16.12: @@ -6583,7 +6603,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.17.8: @@ -6624,7 +6644,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 dev: true /@babel/plugin-transform-spread/7.16.7_@babel+core@7.16.12: @@ -6635,7 +6655,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 dev: false /@babel/plugin-transform-spread/7.16.7_@babel+core@7.17.8: @@ -6676,7 +6696,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.16.12: @@ -6686,7 +6706,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.17.8: @@ -6725,7 +6745,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.16.12: @@ -6735,7 +6755,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.17.8: @@ -6774,7 +6794,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.16.12: @@ -6784,7 +6804,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.17.8: @@ -6828,7 +6848,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-typescript': 7.16.7_@babel+core@7.17.8 transitivePeerDependencies: - supports-color @@ -6863,7 +6883,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.16.12: @@ -6873,7 +6893,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.17.8: @@ -7578,7 +7598,6 @@ packages: dependencies: '@babel/helper-validator-identifier': 7.15.7 to-fast-properties: 2.0.0 - dev: true /@babel/types/7.17.0: resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} @@ -10688,26 +10707,26 @@ packages: optional: true dependencies: '@babel/core': 7.17.8 - '@babel/plugin-proposal-class-properties': 7.16.7_@babel+core@7.17.8 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.17.8 '@babel/plugin-proposal-decorators': 7.16.4_@babel+core@7.17.8 '@babel/plugin-proposal-export-default-from': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-object-rest-spread': 7.17.3_@babel+core@7.17.8 - '@babel/plugin-proposal-optional-chaining': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-private-methods': 7.16.11_@babel+core@7.17.8 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.17.8 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.17.8 - '@babel/plugin-transform-arrow-functions': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-block-scoping': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-classes': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-destructuring': 7.17.7_@babel+core@7.17.8 - '@babel/plugin-transform-for-of': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-shorthand-properties': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-spread': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-template-literals': 7.16.7_@babel+core@7.17.8 + '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.17.8 + '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.17.8 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.17.8 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.17.8 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.17.8 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.17.8 '@babel/preset-env': 7.19.3_@babel+core@7.17.8 '@babel/preset-react': 7.16.7_@babel+core@7.17.8 - '@babel/preset-typescript': 7.16.7_@babel+core@7.17.8 + '@babel/preset-typescript': 7.18.6_@babel+core@7.17.8 '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/channel-postmessage': 6.4.19 @@ -10731,7 +10750,7 @@ packages: babel-plugin-macros: 2.8.0 babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.17.8 case-sensitive-paths-webpack-plugin: 2.4.0 - core-js: 3.21.1 + core-js: 3.25.5 css-loader: 3.6.0_webpack@4.46.0 file-loader: 6.2.0_webpack@4.46.0 find-up: 5.0.0 @@ -12989,7 +13008,7 @@ packages: copy-to-clipboard: 3.3.1 core-js: 3.25.5 core-js-pure: 3.19.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 emotion-theming: 10.3.0_gfrer23gq2rp2t523t6qbxrx6m fuse.js: 3.6.1 global: 4.4.0 @@ -13029,7 +13048,7 @@ packages: copy-to-clipboard: 3.3.1 core-js: 3.25.5 core-js-pure: 3.19.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 emotion-theming: 10.3.0_gfrer23gq2rp2t523t6qbxrx6m fuse.js: 3.6.1 global: 4.4.0 @@ -13058,7 +13077,7 @@ packages: dependencies: '@babel/core': 7.17.8 postcss: 7.0.39 - postcss-syntax: 0.36.2_postcss@8.4.12 + postcss-syntax: 0.36.2_postcss@7.0.39 transitivePeerDependencies: - supports-color dev: true @@ -13071,7 +13090,7 @@ packages: postcss-syntax: '>=0.36.2' dependencies: postcss: 7.0.39 - postcss-syntax: 0.36.2_postcss@8.4.12 + postcss-syntax: 0.36.2_postcss@7.0.39 remark: 13.0.0 unist-util-find-all-after: 3.0.2 transitivePeerDependencies: @@ -13875,7 +13894,7 @@ packages: '@types/wordpress__notices': 3.5.0 '@types/wordpress__rich-text': 3.4.6 '@wordpress/element': 4.8.0 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 re-resizable: 6.9.5_prpqlkd37azqwypxturxi7uyci transitivePeerDependencies: - react @@ -13890,7 +13909,7 @@ packages: '@types/wordpress__notices': 3.5.0 '@types/wordpress__rich-text': 3.4.6 '@wordpress/element': 4.8.0 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 re-resizable: 6.9.5_sfoxds7t5ydpegc3knd667wn6m transitivePeerDependencies: - react @@ -15770,7 +15789,7 @@ packages: classnames: 2.3.1 colord: 2.9.2 dom-scroll-into-view: 1.2.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15823,7 +15842,7 @@ packages: classnames: 2.3.1 colord: 2.9.2 dom-scroll-into-view: 1.2.1 - downshift: 6.1.7_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15877,7 +15896,7 @@ packages: classnames: 2.3.1 colord: 2.9.2 dom-scroll-into-view: 1.2.1 - downshift: 6.1.7_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15988,7 +16007,7 @@ packages: colord: 2.9.2 date-fns: 2.29.3 dom-scroll-into-view: 1.2.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -16042,7 +16061,7 @@ packages: colord: 2.9.2 date-fns: 2.28.0 dom-scroll-into-view: 1.2.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -17860,7 +17879,7 @@ packages: '@wp-g2/styles': 0.0.140_lnjyjqhbidocvrkn4aqhnph4yi '@wp-g2/utils': 0.0.140_wdcame2n4eqmtj7c7r7wzweise csstype: 3.0.10 - downshift: 6.1.9_react@16.14.0 + downshift: 6.1.12_react@16.14.0 framer-motion: 2.9.5_wdcame2n4eqmtj7c7r7wzweise highlight-words-core: 1.2.2 history: 4.10.1 @@ -18789,7 +18808,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.14.7 + follow-redirects: 1.14.5 transitivePeerDependencies: - debug @@ -19157,7 +19176,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.19.3 + '@babel/compat-data': 7.17.7 '@babel/core': 7.12.9 '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.12.9 semver: 6.3.0 @@ -19170,7 +19189,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.19.3 + '@babel/compat-data': 7.17.7 '@babel/core': 7.16.12 '@babel/helper-define-polyfill-provider': 0.3.1_@babel+core@7.16.12 semver: 6.3.0 @@ -19246,7 +19265,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.16.12 + '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.16.12 core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color @@ -20115,7 +20134,7 @@ packages: minipass-pipeline: 1.2.4 mkdirp: 1.0.4 p-map: 4.0.0 - promise-inflight: 1.0.1_bluebird@3.7.2 + promise-inflight: 1.0.1 rimraf: 3.0.2 ssri: 8.0.1 tar: 6.1.11 @@ -21105,7 +21124,7 @@ packages: resolution: {integrity: sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w==} /concat-map/0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /concat-stream/1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -21571,7 +21590,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 2.7.1 semver: 6.3.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 /css-loader/5.2.7_webpack@5.70.0: resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} @@ -22544,6 +22563,31 @@ packages: engines: {node: '>=10'} dev: true + /downshift/6.1.12_react@16.14.0: + resolution: {integrity: sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==} + peerDependencies: + react: '>=16.12.0' + dependencies: + '@babel/runtime': 7.19.0 + compute-scroll-into-view: 1.0.17 + prop-types: 15.8.1 + react: 16.14.0 + react-is: 17.0.2 + tslib: 2.3.1 + dev: false + + /downshift/6.1.12_react@17.0.2: + resolution: {integrity: sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==} + peerDependencies: + react: '>=16.12.0' + dependencies: + '@babel/runtime': 7.19.0 + compute-scroll-into-view: 1.0.17 + prop-types: 15.8.1 + react: 17.0.2 + react-is: 17.0.2 + tslib: 2.3.1 + /downshift/6.1.7_react@17.0.2: resolution: {integrity: sha512-cVprZg/9Lvj/uhYRxELzlu1aezRcgPWBjTvspiGTVEU64gF5pRdSRKFVLcxqsZC637cLAGMbL40JavEfWnqgNg==} peerDependencies: @@ -22570,18 +22614,6 @@ packages: tslib: 2.3.1 dev: false - /downshift/6.1.9_react@17.0.2: - resolution: {integrity: sha512-mzvk61WOX4MEsYHMKCXEVwuz/zM84x/WrCbaCQw71hyNN0fmWXvV673uOQy2idgIA+yqDsjtkV5KPfAFWuQylg==} - peerDependencies: - react: '>=16.12.0' - dependencies: - '@babel/runtime': 7.19.0 - compute-scroll-into-view: 1.0.17 - prop-types: 15.8.1 - react: 17.0.2 - react-is: 17.0.2 - tslib: 2.3.1 - /duplexer/0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -25111,6 +25143,15 @@ packages: readable-stream: 2.3.7 dev: true + /follow-redirects/1.14.5: + resolution: {integrity: sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + /follow-redirects/1.14.7: resolution: {integrity: sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==} engines: {node: '>=4.0'} @@ -25235,7 +25276,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25246,7 +25287,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.5 + semver: 7.3.7 tapable: 1.1.3 typescript: 4.8.4 webpack: 5.70.0 @@ -25266,7 +25307,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25278,7 +25319,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.5 + semver: 7.3.7 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0_webpack-cli@3.3.12 @@ -25330,7 +25371,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25341,7 +25382,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.5 + semver: 7.3.7 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0 @@ -34048,7 +34089,7 @@ packages: dependencies: htmlparser2: 3.10.1 postcss: 7.0.39 - postcss-syntax: 0.36.2_postcss@8.4.12 + postcss-syntax: 0.36.2_postcss@7.0.39 dev: true /postcss-less/3.1.4: @@ -34635,30 +34676,6 @@ packages: postcss: 7.0.39 dev: true - /postcss-syntax/0.36.2_postcss@8.4.12: - resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} - peerDependencies: - postcss: '>=5.0.0' - postcss-html: '*' - postcss-jsx: '*' - postcss-less: '*' - postcss-markdown: '*' - postcss-scss: '*' - peerDependenciesMeta: - postcss-html: - optional: true - postcss-jsx: - optional: true - postcss-less: - optional: true - postcss-markdown: - optional: true - postcss-scss: - optional: true - dependencies: - postcss: 8.4.12 - dev: true - /postcss-unique-selectors/4.0.1: resolution: {integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==} engines: {node: '>=6.9.0'} @@ -36204,6 +36221,24 @@ packages: react-dom: 17.0.2_react@17.0.2 dev: false + /react-with-direction/1.4.0_prpqlkd37azqwypxturxi7uyci: + resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} + peerDependencies: + react: ^0.14 || ^15 || ^16 + react-dom: ^0.14 || ^15 || ^16 + dependencies: + airbnb-prop-types: 2.16.0_react@17.0.2 + brcast: 2.0.2 + deepmerge: 1.5.2 + direction: 1.0.4 + hoist-non-react-statics: 3.3.2 + object.assign: 4.1.4 + object.values: 1.1.5 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 16.14.0_react@17.0.2 + dev: false + /react-with-direction/1.4.0_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} peerDependencies: @@ -36247,7 +36282,7 @@ packages: dependencies: array.prototype.flat: 1.2.5 global-cache: 1.2.1 - react-with-styles: 3.2.3_f6ta4w4ch5ogxra4gx65xzrqki + react-with-styles: 3.2.3_wunono5fri6mu4ojuug6cyhj7m dev: false /react-with-styles-interface-css/6.0.0_sjrqpgd5uboanyy2xkv2xcu6vm: @@ -36284,7 +36319,7 @@ packages: object.assign: 4.1.4 prop-types: 15.8.1 react: 17.0.2 - react-with-direction: 1.4.0_sfoxds7t5ydpegc3knd667wn6m + react-with-direction: 1.4.0_prpqlkd37azqwypxturxi7uyci dev: false /react-with-styles/3.2.3_wunono5fri6mu4ojuug6cyhj7m: @@ -37444,7 +37479,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 semver: 7.3.5 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 /sass-loader/12.6.0_sass@1.49.9+webpack@5.70.0: resolution: {integrity: sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==} @@ -38614,7 +38649,7 @@ packages: dependencies: stylelint: 13.13.1 stylelint-config-recommended: 5.0.0_stylelint@13.13.1 - stylelint-scss: 3.21.0_stylelint@14.6.0 + stylelint-scss: 3.21.0_stylelint@13.13.1 dev: true /stylelint-config-recommended-scss/4.3.0_osgfwlh245rsrcikctalltinom: @@ -38691,7 +38726,7 @@ packages: stylelint: 13.13.1 stylelint-config-recommended: 3.0.0_stylelint@13.13.1 stylelint-config-recommended-scss: 4.3.0_2vkgt733dnumio3be4grtjqkwy - stylelint-scss: 3.21.0_stylelint@14.6.0 + stylelint-scss: 3.21.0_stylelint@13.13.1 dev: true /stylelint-scss/3.21.0_stylelint@13.13.1: @@ -38722,20 +38757,6 @@ packages: stylelint: 13.8.0 dev: true - /stylelint-scss/3.21.0_stylelint@14.6.0: - resolution: {integrity: sha512-CMI2wSHL+XVlNExpauy/+DbUcB/oUZLARDtMIXkpV/5yd8nthzylYd1cdHeDMJVBXeYHldsnebUX6MoV5zPW4A==} - engines: {node: '>=8'} - peerDependencies: - stylelint: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 - dependencies: - lodash: 4.17.21 - postcss-media-query-parser: 0.2.3 - postcss-resolve-nested-selector: 0.1.1 - postcss-selector-parser: 6.0.9 - postcss-value-parser: 4.2.0 - stylelint: 14.6.0 - dev: true - /stylelint-scss/4.2.0_stylelint@14.6.0: resolution: {integrity: sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==} peerDependencies: @@ -38789,7 +38810,7 @@ packages: postcss-sass: 0.4.4 postcss-scss: 2.1.1 postcss-selector-parser: 6.0.6 - postcss-syntax: 0.36.2_postcss@8.4.12 + postcss-syntax: 0.36.2_postcss@7.0.39 postcss-value-parser: 4.1.0 resolve-from: 5.0.0 slash: 3.0.0 @@ -41540,7 +41561,7 @@ packages: tapable: 1.1.3 terser-webpack-plugin: 1.4.5_webpack@4.46.0 watchpack: 1.7.5 - webpack-cli: 3.3.12_webpack@4.46.0 + webpack-cli: 3.3.12_webpack@5.70.0 webpack-sources: 1.4.3 transitivePeerDependencies: - supports-color @@ -41563,7 +41584,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41602,7 +41623,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41643,7 +41664,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41683,7 +41704,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 From a6ed0a0e366603c235616656b3ff8e3304cfe9c9 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 3 Nov 2022 09:47:50 -0700 Subject: [PATCH 136/149] Remove update store details note (#35322) * Remove update store details note * Remove deprecated tests * Remove changelog entry * Remove obsolete note --- .../remove-update-store-details-note | 4 + .../woocommerce/includes/class-wc-install.php | 1 + .../woocommerce/src/Internal/Admin/Events.php | 2 - .../Admin/Notes/UpdateStoreDetails.php | 60 ----------- .../woocommerce-admin/api/admin-notes.php | 99 ------------------- 5 files changed, 5 insertions(+), 161 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove-update-store-details-note delete mode 100644 plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php diff --git a/plugins/woocommerce/changelog/remove-update-store-details-note b/plugins/woocommerce/changelog/remove-update-store-details-note new file mode 100644 index 00000000000..45edb557672 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-update-store-details-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove update store details note diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index e9b8f015ed3..54da7d2fd0e 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -830,6 +830,7 @@ class WC_Install { 'wc-admin-historical-data', 'wc-admin-review-shipping-settings', 'wc-admin-home-screen-feedback', + 'wc-admin-update-store-details', 'wc-admin-effortless-payments-by-mollie', 'wc-admin-google-ads-and-marketing', 'wc-admin-marketing-intro', diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index 1ede546c1e7..f4815f4b4f9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -42,7 +42,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses; use \Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout; use \Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn; use \Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles; -use \Automattic\WooCommerce\Internal\Admin\Notes\UpdateStoreDetails; use \Automattic\WooCommerce\Internal\Admin\Notes\WelcomeToWooCommerceForStoreUsers; use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments; use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommerceSubscriptions; @@ -99,7 +98,6 @@ class Events { RealTimeOrderAlerts::class, TestCheckout::class, TrackingOptIn::class, - UpdateStoreDetails::class, WooCommercePayments::class, WooCommerceSubscriptions::class, ); diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php b/plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php deleted file mode 100644 index f200333753b..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php +++ /dev/null @@ -1,60 +0,0 @@ -set_title( __( 'Edit your store details if you need to', 'woocommerce' ) ); - $note->set_content( __( 'Nice work completing your store profile! You can always go back and edit the details you just shared, as needed.', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'update-store-details', - __( 'Update store details', 'woocommerce' ), - wc_admin_url( '&path=/setup-wizard' ) - ); - - return $note; - } -} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php index d53cc86b611..d582747369f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php @@ -218,79 +218,6 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case { $this->assertEquals( 4, count( $notes ) ); } - /** - * Test getting notes when the user is in tasklist experiment returns notes of size `per_page` without any filters. - * - * @since 3.5.0 - */ - public function test_getting_notes_when_user_is_in_tasklist_experiment_returns_unfiltered_notes() { - // Given. - wp_set_current_user( $this->user ); - WC_Helper_Admin_Notes::reset_notes_dbs(); - // Notes of the following two names are hidden when the user is not in the task list experiment. - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-complete-store-details' ); - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-update-store-details' ); - // Other notes. - WC_Helper_Admin_Notes::add_note_for_test( 'winter-sales' ); - WC_Helper_Admin_Notes::add_note_for_test( '2022-promo' ); - - $this->set_user_in_tasklist_experiment(); - - // When. - $request = new WP_REST_Request( 'GET', $this->endpoint ); - $request->set_query_params( - array( - 'page' => '1', - 'per_page' => '3', - ) - ); - $response = $this->server->dispatch( $request ); - $notes = $response->get_data(); - - // Then. - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 3, count( $notes ) ); - $this->assertEquals( $notes[0]['name'], 'wc-admin-complete-store-details' ); - $this->assertEquals( $notes[1]['name'], 'wc-admin-update-store-details' ); - $this->assertEquals( $notes[2]['name'], 'winter-sales' ); - } - - /** - * Test getting notes when the user is not in tasklist experiment excludes two notes. - * @since 3.5.0 - */ - public function test_getting_notes_when_user_is_not_in_tasklist_experiment_excludes_two_notes() { - $this->markTestSkipped( 'We are disabling the experiments for now.' ); - // Given. - wp_set_current_user( $this->user ); - WC_Helper_Admin_Notes::reset_notes_dbs(); - // Notes of the following two names are hidden when the user is not in the task list experiment. - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-complete-store-details' ); - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-update-store-details' ); - // Other notes. - WC_Helper_Admin_Notes::add_note_for_test( 'summer-sales' ); - WC_Helper_Admin_Notes::add_note_for_test( '2022-promo' ); - - $this->set_user_out_of_tasklist_experiment(); - - // When. - $request = new WP_REST_Request( 'GET', $this->endpoint ); - $request->set_query_params( - array( - 'page' => '1', - 'per_page' => '3', - ) - ); - $response = $this->server->dispatch( $request ); - $notes = $response->get_data(); - - // Then. - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 2, count( $notes ) ); - $this->assertEquals( $notes[0]['name'], 'summer-sales' ); - $this->assertEquals( $notes[1]['name'], '2022-promo' ); - } - /** * Test getting notes of a certain type. * @@ -621,30 +548,4 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 2, count( $notes ) ); } - - /** - * Simulates when the user is in tasklist experiment similar to `API/Notes` `is_tasklist_experiment_assigned_treatment` function. - */ - private function set_user_in_tasklist_experiment() { - // When the user is participating in either `wc-admin-complete-store-details` or `wc-admin-update-store-details` AB tests, - // the user is in tasklist experiment. - update_option( 'woocommerce_allow_tracking', 'yes' ); - $date = new \DateTime(); - $date->setTimeZone( new \DateTimeZone( 'UTC' ) ); - - $experiment_name = sprintf( - 'woocommerce_tasklist_progression_headercard_%s_%s', - $date->format( 'Y' ), - $date->format( 'm' ) - ); - set_transient( 'abtest_variation_' . $experiment_name, 'treatment' ); - } - - /** - * Simulates when the user is not in tasklist experiment similar to `API/Notes` `is_tasklist_experiment_assigned_treatment` function. - */ - private function set_user_out_of_tasklist_experiment() { - // Any experiment is off when `woocommerce_allow_tracking` option is false. - update_option( 'woocommerce_allow_tracking', false ); - } } From e8db853ceb10e1a21787a5f4171055ee214c6b47 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 3 Nov 2022 09:48:20 -0700 Subject: [PATCH 137/149] Remove insight on first sale note (#35341) * Remove insight on first sale note * Add changelog entry --- .../changelog/remove-insight-first-sale-note | 4 ++ .../woocommerce/includes/class-wc-install.php | 1 + .../src/Admin/Notes/DeprecatedNotes.php | 21 ------ .../woocommerce/src/Internal/Admin/Events.php | 2 - .../Internal/Admin/Notes/InsightFirstSale.php | 71 ------------------- 5 files changed, 5 insertions(+), 94 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove-insight-first-sale-note delete mode 100644 plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php diff --git a/plugins/woocommerce/changelog/remove-insight-first-sale-note b/plugins/woocommerce/changelog/remove-insight-first-sale-note new file mode 100644 index 00000000000..91145d27806 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-insight-first-sale-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove insight on first sale note diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 54da7d2fd0e..8fe7066711f 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -833,6 +833,7 @@ class WC_Install { 'wc-admin-update-store-details', 'wc-admin-effortless-payments-by-mollie', 'wc-admin-google-ads-and-marketing', + 'wc-admin-insight-first-sale', 'wc-admin-marketing-intro', 'wc-admin-draw-attention', 'wc-admin-need-some-inspiration', diff --git a/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php b/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php index 2920f0880b0..baf2645ada9 100644 --- a/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php +++ b/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php @@ -223,27 +223,6 @@ class WC_Admin_Notes_Giving_Feedback_Notes extends DeprecatedClassFacade { protected static $deprecated_in_version = '4.8.0'; } -/** - * WC_Admin_Notes_Insight_First_Sale. - * - * @deprecated since 4.8.0, use InsightFirstSale - */ -class WC_Admin_Notes_Insight_First_Sale extends DeprecatedClassFacade { - /** - * The name of the non-deprecated class that this facade covers. - * - * @var string - */ - protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstSale'; - - /** - * The version that this class was deprecated in. - * - * @var string - */ - protected static $deprecated_in_version = '4.8.0'; -} - /** * WC_Admin_Notes_Install_JP_And_WCS_Plugins. * diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index f4815f4b4f9..1a1e57231ab 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -20,7 +20,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove; use \Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstDownloadableProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct; -use \Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstSale; use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use \Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist; use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; @@ -82,7 +81,6 @@ class Events { EUVATNumber::class, FirstDownloadableProduct::class, FirstProduct::class, - InsightFirstSale::class, LaunchChecklist::class, MagentoMigration::class, ManageOrdersOnTheGo::class, diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php b/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php deleted file mode 100644 index 1cc28010536..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php +++ /dev/null @@ -1,71 +0,0 @@ -set_title( __( 'Did you know?', 'woocommerce' ) ); - $note->set_content( __( 'A WooCommerce powered store needs on average 31 days to get the first sale. You\'re on the right track! Do you find this type of insight useful?', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_SURVEY ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - - // Note that there is no corresponding function called in response to - // this. Apart from setting the note to actioned a tracks event is - // sent in NoteActions. - $note->add_action( - 'affirm-insight-first-sale', - __( 'Yes', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - $note->add_action( - 'deny-insight-first-sale', - __( 'No', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - - return $note; - } -} From f0ee7a48822ceba77a599cae8a2f251c2f7f1651 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Thu, 3 Nov 2022 12:54:17 -0400 Subject: [PATCH 138/149] DateTimePickerControl: Only call onChange when the date actually changes (#35397) * Additional onChange tests * Update handling when currentDate or dateTimeFormat changes to avoid errant onChange calls * Remove unused changeImmediate function * Refactor input change handling * Refactoring of updateState* functions to reduce duplication and increase readability * Refactor to remove a few unnecessary wrapper functions * Rename setInputStringWithMoment to formatDateTimeForDisplay * Refactor ISO formatting * Rename parse functions * Update controlled stories to properly use state * Refactored to get rid of useEffect handling of currentDate and inputString changes --- .../fix-date-time-picker-control-onchange | 4 + .../date-time-picker-control.tsx | 271 ++++++++---------- .../stories/index.tsx | 79 +++-- .../date-time-picker-control/test/index.tsx | 108 +++++++ 4 files changed, 287 insertions(+), 175 deletions(-) create mode 100644 packages/js/components/changelog/fix-date-time-picker-control-onchange diff --git a/packages/js/components/changelog/fix-date-time-picker-control-onchange b/packages/js/components/changelog/fix-date-time-picker-control-onchange new file mode 100644 index 00000000000..f7074ae252b --- /dev/null +++ b/packages/js/components/changelog/fix-date-time-picker-control-onchange @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +DateTimePickerControl's onChange now only fires when there is an actual change to the datetime. diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index b53a34d9ed7..b133d453ade 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -4,9 +4,10 @@ import { format as formatDate } from '@wordpress/date'; import { createElement, + useCallback, useState, useEffect, - useLayoutEffect, + useMemo, useRef, } from '@wordpress/element'; import { Icon, calendar } from '@wordpress/icons'; @@ -30,7 +31,7 @@ export const default12HourDateTimeFormat = 'm/d/Y h:i a'; export const default24HourDateTimeFormat = 'm/d/Y H:i'; export type DateTimePickerControlOnChangeHandler = ( - date: string, + dateTimeIsoString: string, isValid: boolean ) => void; @@ -68,20 +69,9 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { const id = `inspector-date-time-picker-control-${ instanceId }`; const inputControl = useRef< InputControl >(); - const isMounted = useRef( false ); - useEffect( () => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - } ); - const [ inputString, setInputString ] = useState( '' ); - const [ lastValidDate, setLastValidDate ] = useState< Moment | null >( - null - ); - const displayFormat = ( () => { + const displayFormat = useMemo( () => { if ( dateTimeFormat ) { return dateTimeFormat; } @@ -95,46 +85,42 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } return default24HourDateTimeFormat; - } )(); + }, [ dateTimeFormat, isDateOnlyPicker, is12HourPicker ] ); - function parseMomentIso( + function parseAsISODateTime( dateString?: string | null, assumeLocalTime = false ): Moment { - if ( assumeLocalTime ) { - return moment( dateString, moment.ISO_8601, true ).utc(); - } - - return moment.utc( dateString, moment.ISO_8601, true ); + return assumeLocalTime + ? moment( dateString, moment.ISO_8601, true ).utc() + : moment.utc( dateString, moment.ISO_8601, true ); } - function parseMoment( dateString: string | null ): Moment { + function parseAsLocalDateTime( dateString: string | null ): Moment { // parse input date string as local time; // be lenient of user input and try to match any format Moment can return moment( dateString ); } - function formatMomentIso( momentDate: Moment ): string { - return momentDate.utc().toISOString(); - } + const maybeForceTime = useCallback( + ( momentDate: Moment ) => { + if ( ! isDateOnlyPicker || ! momentDate.isValid() ) + return momentDate; - function formatMoment( momentDate: Moment ): string { - return formatDate( displayFormat, momentDate.local() ); - } + // We want to set to the start/end of the local time, so + // we need to put our Moment instance into "local" mode + const updatedMomentDate = momentDate.clone().local(); - function maybeForceTime( momentDate: Moment ): Moment { - if ( ! isDateOnlyPicker ) return momentDate; + if ( timeForDateOnly === 'start-of-day' ) { + updatedMomentDate.startOf( 'day' ); + } else if ( timeForDateOnly === 'end-of-day' ) { + updatedMomentDate.endOf( 'day' ); + } - const updatedMomentDate = momentDate.clone(); - - if ( timeForDateOnly === 'start-of-day' ) { - updatedMomentDate.startOf( 'day' ); - } else if ( timeForDateOnly === 'end-of-day' ) { - updatedMomentDate.endOf( 'day' ); - } - - return updatedMomentDate; - } + return updatedMomentDate; + }, + [ isDateOnlyPicker, timeForDateOnly ] + ); function hasFocusLeftInputAndDropdownContent( event: React.FocusEvent< HTMLInputElement > @@ -144,101 +130,67 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { ); } - // We setup the debounced handling of the input string changes using - // useRef because useCallback does *not* guarantee that the resulting - // callback function will not be recreated, even if the dependencies - // haven't changed (this is because of it's use of useMemo under the - // hood, which also makes not guarantee). And, even if it did, the - // equality check for useCallback dependencies is by reference. So, if - // the "same" function is passed in, but it is a different instance, it - // will trigger the recreation of the callback. - // - // With useDebounce, if the callback function changes, the current - // debounce is canceled. This results in the callback function never being - // called. - // - // We *need* to ensure that our handler is called at least once, - // and also that we call the passed in onChange callback. - // - // We guarantee this by keeping references to both our handler and the - // passed in prop. - // - // The consumer of DateTimePickerControl should ensure that the - // function passed into onChange does not change (using references or - // useCallbackOne). But, even if they do not, and the function changes, - // things will likely function as expected unless the consumer is doing - // something really convoluted. - // - // See also: - // - [note regarding useMemo not being a semantic guarantee](https://reactjs.org/docs/hooks-reference.html#usememo) - // - [useDebounce hook loses function calls if the dependency changes](https://github.com/WordPress/gutenberg/issues/35505) - // - [useMemoOne and useCallbackOne](https://github.com/alexreardon/use-memo-one) - - const onChangePropFunctionRef = useRef< - DateTimePickerControlOnChangeHandler | undefined - >(); - useLayoutEffect( () => { - onChangePropFunctionRef.current = onChange; - }, [ onChange ] ); - - function inputStringChangeHandlerFunction( - newInputString: string, - fireOnChange: boolean - ) { - if ( ! isMounted.current ) return; - - let newDateTime = parseMoment( newInputString ); - const isValid = newDateTime.isValid(); - - if ( isValid ) { - newDateTime = maybeForceTime( newDateTime ); - setLastValidDate( newDateTime ); - } - - if ( - fireOnChange && - typeof onChangePropFunctionRef.current === 'function' - ) { - onChangePropFunctionRef.current( - isValid ? formatMomentIso( newDateTime ) : newInputString, - isValid - ); - } - } - - const inputStringChangeHandlerFunctionRef = useRef< - ( newInputString: string, fireOnChange: boolean ) => void - >( inputStringChangeHandlerFunction ); - // whenever forceTimeTo changes, we need to reset the ref to inputStringChangeHandlerFunction - // so that we are using the most current forceTimeTo value inside of it - useEffect( () => { - inputStringChangeHandlerFunctionRef.current = - inputStringChangeHandlerFunction; - }, [ timeForDateOnly ] ); - - const debouncedInputStringChangeHandler = useDebounce( - inputStringChangeHandlerFunctionRef.current, - onChangeDebounceWait + const formatDateTimeForDisplay = useCallback( + ( dateTime: Moment ) => { + return dateTime.isValid() + ? formatDate( displayFormat, dateTime.local() ) + : dateTime.creationData().input?.toString() || ''; + }, + [ displayFormat ] ); - function change( newInputString: string ) { - setInputString( newInputString ); - debouncedInputStringChangeHandler( newInputString, true ); + function formatDateTimeAsISO( dateTime: Moment ): string { + return dateTime.isValid() + ? dateTime.utc().toISOString() + : dateTime.creationData().input?.toString() || ''; } - function changeImmediate( newInputString: string, fireOnChange: boolean ) { - setInputString( newInputString ); - inputStringChangeHandlerFunctionRef.current( - newInputString, - fireOnChange - ); - } + const inputStringDateTime = useMemo( () => { + return maybeForceTime( parseAsLocalDateTime( inputString ) ); + }, [ inputString, maybeForceTime ] ); - function blur() { - if ( onBlur ) { - onBlur(); - } - } + // We keep a ref to the onChange prop so that we can be sure we are + // always using the more up-to-date value, even if it changes + // it while a debounced onChange handler is in progress + const onChangeRef = useRef< + DateTimePickerControlOnChangeHandler | undefined + >(); + useEffect( () => { + onChangeRef.current = onChange; + }, [ onChange ] ); + + const setInputStringAndMaybeCallOnChange = useCallback( + ( newInputString: string, isUserTypedInput: boolean ) => { + const newDateTime = maybeForceTime( + isUserTypedInput + ? parseAsLocalDateTime( newInputString ) + : parseAsISODateTime( newInputString, true ) + ); + const isDateTimeSame = newDateTime.isSame( inputStringDateTime ); + + if ( isUserTypedInput ) { + setInputString( newInputString ); + } else if ( ! isDateTimeSame ) { + setInputString( formatDateTimeForDisplay( newDateTime ) ); + } + + if ( + typeof onChangeRef.current === 'function' && + ! isDateTimeSame + ) { + onChangeRef.current( + formatDateTimeAsISO( newDateTime ), + newDateTime.isValid() + ); + } + }, + [ formatDateTimeForDisplay, inputStringDateTime, maybeForceTime ] + ); + + const debouncedSetInputStringAndMaybeCallOnChange = useDebounce( + setInputStringAndMaybeCallOnChange, + onChangeDebounceWait + ); function focusInputControl() { if ( inputControl.current ) { @@ -246,21 +198,23 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } } - const isInitialUpdate = useRef( true ); - useEffect( () => { - const fireOnChange = ! isInitialUpdate.current; - if ( isInitialUpdate.current ) { - isInitialUpdate.current = false; + function getUserInputOrUpdatedCurrentDate() { + const newDateTime = maybeForceTime( + parseAsISODateTime( currentDate, false ) + ); + + if ( + ! newDateTime.isValid() || + newDateTime.isSame( + maybeForceTime( parseAsLocalDateTime( inputString ) ) + ) + ) { + // keep the input string as the user entered it + return inputString; } - const newDate = parseMomentIso( currentDate ); - - if ( newDate.isValid() ) { - changeImmediate( formatMoment( newDate ), fireOnChange ); - } else { - changeImmediate( currentDate || '', fireOnChange ); - } - }, [ currentDate, displayFormat, timeForDateOnly ] ); + return formatDateTimeForDisplay( newDateTime ); + } return ( = ( { focusOnMount={ false } // @ts-expect-error `onToggle` does exist. onToggle={ ( willOpen ) => { - if ( ! willOpen ) { - blur(); + if ( ! willOpen && typeof onBlur === 'function' ) { + onBlur(); } } } renderToggle={ ( { isOpen, onToggle } ) => ( @@ -282,8 +236,13 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { id={ id } ref={ inputControl } disabled={ disabled } - value={ inputString } - onChange={ change } + value={ getUserInputOrUpdatedCurrentDate() } + onChange={ ( newValue: string ) => + debouncedSetInputStringAndMaybeCallOnChange( + newValue, + true + ) + } onBlur={ ( event: React.FocusEvent< HTMLInputElement > ) => { @@ -323,21 +282,21 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { ) } renderContent={ () => { const Picker = isDateOnlyPicker ? DatePicker : WpDateTimePicker; + const inputDateTime = parseAsLocalDateTime( inputString ); return ( { - // the picker returns the date in local time - const formattedDate = formatMoment( - parseMomentIso( date, true ) - ); - changeImmediate( formattedDate, true ); - } } + onChange={ ( newDateTimeISOString: string ) => + setInputStringAndMaybeCallOnChange( + newDateTimeISOString, + false + ) + } is12Hour={ is12HourPicker } /> ); diff --git a/packages/js/components/src/date-time-picker-control/stories/index.tsx b/packages/js/components/src/date-time-picker-control/stories/index.tsx index 937f8e4dadd..adcf77f1336 100644 --- a/packages/js/components/src/date-time-picker-control/stories/index.tsx +++ b/packages/js/components/src/date-time-picker-control/stories/index.tsx @@ -38,8 +38,15 @@ CustomDateTimeFormat.args = { }; function ControlledContainer( { children, ...props } ) { + function nowWithZeroedSeconds() { + const now = new Date(); + now.setSeconds( 0 ); + now.setMilliseconds( 0 ); + return now; + } + const [ controlledDate, setControlledDate ] = useState( - new Date().toISOString() + nowWithZeroedSeconds().toISOString() ); return ( @@ -48,11 +55,19 @@ function ControlledContainer( { children, ...props } ) {
+
+
+ Controlled date: +
{ controlledDate } +
+
); @@ -70,28 +85,54 @@ CustomClassName.args = { className: 'custom-class-name', }; +function ControlledDecorator( Story, props ) { + function nowWithZeroedSeconds() { + const now = new Date(); + now.setSeconds( 0 ); + now.setMilliseconds( 0 ); + return now; + } + + const [ controlledDate, setControlledDate ] = useState( + nowWithZeroedSeconds().toISOString() + ); + + return ( +
+ +
+ +
+
+ Controlled date: +
{ controlledDate } +
+
+
+
+ ); +} + export const Controlled = Template.bind( {} ); Controlled.args = { ...Basic.args, help: "I'm controlled by a container that uses React state", }; -Controlled.decorators = [ - ( story, props ) => { - return ( - - { ( controlledDate, setControlledDate ) => - story( { - args: { - currentDate: controlledDate, - onChange: setControlledDate, - ...props.args, - }, - } ) - } - - ); - }, -]; +Controlled.decorators = [ ControlledDecorator ]; export const ControlledDateOnly = Template.bind( {} ); ControlledDateOnly.args = { diff --git a/packages/js/components/src/date-time-picker-control/test/index.tsx b/packages/js/components/src/date-time-picker-control/test/index.tsx index e0fcbfd5186..1088e875feb 100644 --- a/packages/js/components/src/date-time-picker-control/test/index.tsx +++ b/packages/js/components/src/date-time-picker-control/test/index.tsx @@ -659,4 +659,112 @@ describe( 'DateTimePickerControl', () => { await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); } ); + + it( 'should not call onChange if currentDate is set to an equivalent UTC date without Zulu offset specifier', async () => { + const originalDateTime = '2023-01-01T00:00:00Z'; + const equivalentDateTimeWithoutZulu = '2023-01-01T00:00:00'; + const onChangeHandler = jest.fn(); + + const { rerender } = render( + + ); + + // re-render the component; we do this to then test whether our onChange still gets called + rerender( + + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); + } ); + + it( 'should not call onChange if currentDate is set to an equivalent UTC date without time', async () => { + const originalDateTime = '2023-01-01T00:00:00Z'; + const equivalentDateTimeWithoutTime = '2023-01-01'; + const onChangeHandler = jest.fn(); + + const { rerender } = render( + + ); + + // re-render the component; we do this to then test whether our onChange still gets called + rerender( + + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); + } ); + + it( 'should not call onChange when the dateTimeFormat changes', async () => { + // we are specifically using a date with seconds in it, with a format + // without seconds in it; this helps us to determine if the currentDate + // is getting re-parsed from the input string (if it does this, it + // would result in a different date) + const originalDateTime = moment( '2022-11-15 02:30:40' ); + const originalDateTimeFormat = 'm-d-Y, H:i'; + const newDateTimeFormat = 'Y-m-d H:i'; + const onChangeHandler = jest.fn(); + + const { rerender } = render( + + ); + + // re-render the component; we do this to then test whether our onChange still gets called + rerender( + + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); + } ); + + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should not call onChange when the input is changed to an equivalent date', async () => { + const originalDateTime = moment( '2022-09-15' ); + const newDateTimeInputString = 'September 9, 2022'; + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled(), { + timeout: 10000, + } ); + }, 10000 ); } ); From a38a7df76c92886310c1a430ca420adb3a49aa5c Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 3 Nov 2022 12:25:42 -0700 Subject: [PATCH 139/149] Remove adding and managing products note (#35319) * Remove adding and managing products note * Add changelog entry * Remove obsolete note --- .../remove-adding-and-managing-products-note | 4 + .../woocommerce/includes/class-wc-install.php | 1 + .../woocommerce/src/Internal/Admin/Events.php | 2 - .../Notes/AddingAndManangingProducts.php | 80 ------------------- 4 files changed, 5 insertions(+), 82 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove-adding-and-managing-products-note delete mode 100644 plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php diff --git a/plugins/woocommerce/changelog/remove-adding-and-managing-products-note b/plugins/woocommerce/changelog/remove-adding-and-managing-products-note new file mode 100644 index 00000000000..b9cf435de9e --- /dev/null +++ b/plugins/woocommerce/changelog/remove-adding-and-managing-products-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove adding and managing products note diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 8fe7066711f..33fd81dd1cf 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -826,6 +826,7 @@ class WC_Install { 'wc-admin-store-notice-setting-moved', 'wc-admin-store-notice-giving-feedback', 'wc-admin-learn-more-about-product-settings', + 'wc-admin-adding-and-managing-products', 'wc-admin-onboarding-profiler-reminder', 'wc-admin-historical-data', 'wc-admin-review-shipping-settings', diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index 1a1e57231ab..edd220b7d72 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -11,7 +11,6 @@ use \Automattic\WooCommerce\Admin\Features\Features; use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\DataSourcePoller; use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine; use \Automattic\WooCommerce\Internal\Admin\Notes\AddFirstProduct; -use \Automattic\WooCommerce\Internal\Admin\Notes\AddingAndManangingProducts; use \Automattic\WooCommerce\Internal\Admin\Notes\ChoosingTheme; use \Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved; use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks; @@ -73,7 +72,6 @@ class Events { */ private static $note_classes_to_added_or_updated = array( AddFirstProduct::class, - AddingAndManangingProducts::class, ChoosingTheme::class, CustomizeStoreWithBlocks::class, CustomizingProductCatalog::class, diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php b/plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php deleted file mode 100644 index 6dba24c73d8..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php +++ /dev/null @@ -1,80 +0,0 @@ - 3 days and - * product count is 0 - * - * @package WooCommerce\Admin - */ - -namespace Automattic\WooCommerce\Internal\Admin\Notes; - -defined( 'ABSPATH' ) || exit; - -use \Automattic\WooCommerce\Admin\Notes\Note; -use \Automattic\WooCommerce\Admin\Notes\NoteTraits; - -/** - * Class AddingAndManangingProducts - * - * @package Automattic\WooCommerce\Admin\Notes - */ -class AddingAndManangingProducts { - /** - * Note traits. - */ - use NoteTraits; - - /** - * Name of the note for use in the database. - */ - const NOTE_NAME = 'wc-admin-adding-and-managing-products'; - - /** - * Get the note. - * - * @return Note|null - */ - public static function get_note() { - // Store must have been at least 3 days. - if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS * 3 ) ) { - return; - } - - // Total # of products must be 0. - $query = new \WC_Product_Query( - array( - 'limit' => 1, - 'paginate' => true, - 'return' => 'ids', - 'status' => array( 'publish' ), - ) - ); - - $products = $query->get_products(); - if ( 0 !== $products->total ) { - return; - } - - $note = new Note(); - $note->set_title( __( 'Adding and Managing Products', 'woocommerce' ) ); - $note->set_content( - __( - 'Learn more about how to set up products in WooCommerce through our useful documentation about adding and managing products.', - 'woocommerce' - ) - ); - $note->set_content_data( (object) array() ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'learn-more', - __( 'Learn more', 'woocommerce' ), - 'https://woocommerce.com/document/managing-products/?utm_source=inbox&utm_medium=product' - ); - - return $note; - } -} From ad1c49f9e4c49b2e90f9fb56f6f642295e32b4c3 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 3 Nov 2022 12:27:09 -0700 Subject: [PATCH 140/149] Remove manage store activity from home screen note (#35320) * Remove manage store activity note * Add changelog entry * Remove obsolete note --- ...anage-store-activity-from-home-screen-note | 4 ++ .../woocommerce/includes/class-wc-install.php | 1 + .../woocommerce/src/Internal/Admin/Events.php | 2 - .../src/Internal/Admin/FeaturePlugin.php | 2 - .../ManageStoreActivityFromHomeScreen.php | 68 ------------------- 5 files changed, 5 insertions(+), 72 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note delete mode 100644 plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php diff --git a/plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note b/plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note new file mode 100644 index 00000000000..81c212c40a3 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove manage store activity note diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 33fd81dd1cf..437222d4216 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -829,6 +829,7 @@ class WC_Install { 'wc-admin-adding-and-managing-products', 'wc-admin-onboarding-profiler-reminder', 'wc-admin-historical-data', + 'wc-admin-manage-store-activity-from-home-screen', 'wc-admin-review-shipping-settings', 'wc-admin-home-screen-feedback', 'wc-admin-update-store-details', diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index edd220b7d72..0d252d75936 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -23,7 +23,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use \Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist; use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; use \Automattic\WooCommerce\Internal\Admin\Notes\ManageOrdersOnTheGo; -use \Automattic\WooCommerce\Internal\Admin\Notes\ManageStoreActivityFromHomeScreen; use \Automattic\WooCommerce\Internal\Admin\Notes\MarketingJetpack; use \Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications; use \Automattic\WooCommerce\Internal\Admin\Notes\MigrateFromShopify; @@ -106,7 +105,6 @@ class Events { private static $other_note_classes = array( CouponPageMoved::class, InstallJPAndWCSPlugins::class, - ManageStoreActivityFromHomeScreen::class, OrderMilestones::class, SellingOnlineCourses::class, UnsecuredReportFiles::class, diff --git a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php index f9e94523282..ad67eded950 100644 --- a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php +++ b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php @@ -18,7 +18,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout; use \Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses; use \Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications; use \Automattic\WooCommerce\Internal\Admin\Notes\WelcomeToWooCommerceForStoreUsers; -use \Automattic\WooCommerce\Internal\Admin\Notes\ManageStoreActivityFromHomeScreen; use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\PluginsHelper; @@ -182,7 +181,6 @@ class FeaturePlugin { new TestCheckout(); new SellingOnlineCourses(); new WelcomeToWooCommerceForStoreUsers(); - new ManageStoreActivityFromHomeScreen(); new MagentoMigration(); // Initialize MerchantEmailNotifications. diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php b/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php deleted file mode 100644 index 026f6bf5024..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php +++ /dev/null @@ -1,68 +0,0 @@ -set_title( __( 'New! Manage your store activity from the Home screen', 'woocommerce' ) ); - $note->set_content( __( 'Start your day knowing the next steps you need to take with your orders, products, and customer feedback.

Read more about how to use the activity panels on the Home screen.', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'learn-more', - __( 'Learn more', 'woocommerce' ), - 'https://woocommerce.com/document/home-screen/?utm_source=inbox&utm_medium=product', - Note::E_WC_ADMIN_NOTE_ACTIONED - ); - - return $note; - } -} From c62f9843b92d4c2aeba9c9af3fb91af1ee13977a Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 3 Nov 2022 13:13:01 -0700 Subject: [PATCH 141/149] Remove the first downloadable product note (#35318) * Remove first downloadable product note * Add changelog entry * Remove obsolete note --- .../remove-first-downloadable-product-note | 4 ++ .../woocommerce/includes/class-wc-install.php | 1 + .../woocommerce/src/Internal/Admin/Events.php | 2 - .../Admin/Notes/FirstDownloadableProduct.php | 71 ------------------- 4 files changed, 5 insertions(+), 73 deletions(-) create mode 100644 plugins/woocommerce/changelog/remove-first-downloadable-product-note delete mode 100644 plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php diff --git a/plugins/woocommerce/changelog/remove-first-downloadable-product-note b/plugins/woocommerce/changelog/remove-first-downloadable-product-note new file mode 100644 index 00000000000..dd51ea60e1f --- /dev/null +++ b/plugins/woocommerce/changelog/remove-first-downloadable-product-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove first downloadable product note diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 437222d4216..594fb2f7cac 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -825,6 +825,7 @@ class WC_Install { 'wc-admin-insight-first-product-and-payment', 'wc-admin-store-notice-setting-moved', 'wc-admin-store-notice-giving-feedback', + 'wc-admin-first-downloadable-product', 'wc-admin-learn-more-about-product-settings', 'wc-admin-adding-and-managing-products', 'wc-admin-onboarding-profiler-reminder', diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index 0d252d75936..94799e91ebd 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -17,7 +17,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks; use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizingProductCatalog; use \Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove; use \Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber; -use \Automattic\WooCommerce\Internal\Admin\Notes\FirstDownloadableProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use \Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist; @@ -76,7 +75,6 @@ class Events { CustomizingProductCatalog::class, EditProductsOnTheMove::class, EUVATNumber::class, - FirstDownloadableProduct::class, FirstProduct::class, LaunchChecklist::class, MagentoMigration::class, diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php b/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php deleted file mode 100644 index e38f22ce385..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownloadableProduct.php +++ /dev/null @@ -1,71 +0,0 @@ - 1, - 'paginate' => true, - 'return' => 'ids', - 'downloadable' => 1, - 'status' => array( 'publish' ), - ) - ); - $products = $query->get_products(); - - // There must be at least 1 downloadable product. - if ( 0 === $products->total ) { - return; - } - - $note = new Note(); - $note->set_title( __( 'Learn more about digital/downloadable products', 'woocommerce' ) ); - $note->set_content( - __( - 'Congrats on adding your first digital product! You can learn more about how to handle digital or downloadable products in our documentation.', - 'woocommerce' - ) - ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'first-downloadable-product-handling', - __( 'Learn more', 'woocommerce' ), - 'https://woocommerce.com/document/digital-downloadable-product-handling/?utm_source=inbox&utm_medium=product' - ); - - return $note; - } -} From a6921dd4b6104226381b99ea16eff990d5eb7008 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 07:36:37 -0500 Subject: [PATCH 142/149] Update changelog.txt from release 7.0.1 (#35457) Co-authored-by: WooCommerce Bot --- changelog.txt | 90 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/changelog.txt b/changelog.txt index e30f9f9135c..04e7e45ae80 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,21 @@ == Changelog == += 7.0.1 2022-11-01 = + +**WooCommerce** + +* Dev - Twenty Twenty-Three theme compatibility. [#35306](https://github.com/woocommerce/woocommerce/pull/35306) +* Dev - Simplify and reduce size of payload supplied by the woocommerce_get_customer_details ajax endpoint. + +**WooCommerce Blocks 8.5.2** + +* Enhancement - Fix Mini Cart Global Styles. [7515](https://github.com/woocommerce/woocommerce-blocks/pull/7515) +* Enhancement - Fix inconsistent button styling with TT3. ([7516](https://github.com/woocommerce/woocommerce-blocks/pull/7516)) +* Enhancement - Make the Filter by Price block range color dependent of the theme color. [7525](https://github.com/woocommerce/woocommerce-blocks/pull/7525) +* Enhancement - Filter by Price block: fix price slider visibility on dark themes. [7527](https://github.com/woocommerce/woocommerce-blocks/pull/7527) +* Enhancement - Update the Mini Cart block drawer to honor the theme's background. [7510](https://github.com/woocommerce/woocommerce-blocks/pull/7510) +* Enhancement - Add white background to Filter by Attribute block dropdown so text is legible in dark backgrounds. [7506](https://github.com/woocommerce/woocommerce-blocks/pull/7506) + = 7.0.0 2022-10-11 = **WooCommerce** @@ -51,7 +67,6 @@ * Add - Support order searches as an integral part of how (COT) order queries work. [#34405](https://github.com/woocommerce/woocommerce/pull/34405) * Add - UI Revamp on Marketing Page with feature toggle. [#34642](https://github.com/woocommerce/woocommerce/pull/34642) * Add - Woo Mobile Welcome Page with Magic Link feature [#34637](https://github.com/woocommerce/woocommerce/pull/34637) -* Update - Update WooCommerce Blocks to 8.5.1 [#34807](https://github.com/woocommerce/woocommerce/pull/34807) * Update - Updates tracking parameters for marketing messages of mobile apps in New order mail. [#34717](https://github.com/woocommerce/woocommerce/pull/34717) * Update - Add an empty list of states for Saint Martin (French part) [#34521](https://github.com/woocommerce/woocommerce/pull/34521) * Update - Add Wish and Walmart to the platform options [#34541](https://github.com/woocommerce/woocommerce/pull/34541) @@ -172,7 +187,7 @@ * Fix - Minor changes to address Typescript errors after updating TS definitions [#34154](https://github.com/woocommerce/woocommerce/pull/34154) * Fix - Refactored homescreen component to use useQuery hook [#34183](https://github.com/woocommerce/woocommerce/pull/34183) * Fix - Support Cart/Checkout/My accounts/Terms settings in WC REST API [#34234](https://github.com/woocommerce/woocommerce/pull/34234) -* Fix - Use the default payment suggestions when woocommerce_show_marketplace_suggestions is set to no [#34083](https://github.com/woocommerce/woocommerce/pull/34083) +* Fix - Use the default paymetn suggestions when woocommerce_show_marketplace_suggestions is set to no [#34083](https://github.com/woocommerce/woocommerce/pull/34083) * Fix - Wrap default payment gateway strings in __() function call [#33987](https://github.com/woocommerce/woocommerce/pull/33987) * Add - Add default styles for block themes to ensure WooCommerce looks better out of the box with block themes that are not optimized for WooCommerce specifically. [#33518](https://github.com/woocommerce/woocommerce/pull/33518) * Add - Added tour for store location [#34137](https://github.com/woocommerce/woocommerce/pull/34137) @@ -245,9 +260,8 @@ * Enhancement - Reduce the amount of terms shown in attributes page [#33962](https://github.com/woocommerce/woocommerce/pull/33962) * Enhancement - Use method_exists instead of throwing in AbstractServiceProvider::reflect_class_or_callable [#33960](https://github.com/woocommerce/woocommerce/pull/33960) -**WooCommerce Blocks 8.1.0 & 8.2.0 & 8.2.1 & 8.3.0 & 8.3.1 & 8.3.2** +**WooCommerce Blocks 8.1.0 & 8.2.0 & 8.2.1 & 8.3.0 & 8.3.1** -* Enhancement - Add feedback box to the Cart & Checkout Inner Blocks in the inspector. ([6881](https://github.com/woocommerce/woocommerce-blocks/pull/6881)) * Enhancement - Enable the Cart and Checkout blocks when WooCommerce Blocks is bundled in WooCommerce Core.([6805](https://github.com/woocommerce/woocommerce-blocks/pull/6805)) * Enhancement - Refactor style-attributes hooks to add as global custom imports and remove relative import paths.([6870](https://github.com/woocommerce/woocommerce-blocks/pull/6870)) * Enhancement - Add the ability to register patterns by adding them under the "patterns" folder and add the new "WooCommerce Filters" pattern.([6861](https://github.com/woocommerce/woocommerce-blocks/pull/6861)) @@ -256,8 +270,11 @@ * Enhancement - Add filter URL support to filter blocks when filtering for All Products block.([6642](https://github.com/woocommerce/woocommerce-blocks/pull/6642)) * Enhancement - Add: Allow choosing between single and multiple sections.([6620](https://github.com/woocommerce/woocommerce-blocks/pull/6620)) * Enhancement - Cart endpoint for Store API (/wc/store/cart) now features cross-sell items based on cart contents.([6635](https://github.com/woocommerce/woocommerce-blocks/pull/6635)) -* Fix - Fix Best Selling Products block ordering ([7025](https://github.com/woocommerce/woocommerce-blocks/pull/7025)) -* Fix - Prevent unnecessarily showing the item names in a shipping package if it's the only package. ([6899](https://github.com/woocommerce/woocommerce-blocks/pull/6899)) +* Enhancement - Add feedback box to the Cart & Checkout Inner Blocks in the inspector. ([6881](https://github.com/woocommerce/woocommerce-blocks/pull/6881)) +* Enhancement - Refactor style-attributes hooks to add as global custom imports and remove relative import paths. ([6870](https://github.com/woocommerce/woocommerce-blocks/pull/6870)) +* Enhancement - Add notice to Cart and Checkout blocks' inspector controls which links to the list of compatible plugins. ([6869](https://github.com/woocommerce/woocommerce-blocks/pull/6869)) +* Enhancement - Add the ability to register patterns by adding them under the "patterns" folder and add the new "WooCommerce Filters" pattern. ([6861](https://github.com/woocommerce/woocommerce-blocks/pull/6861)) +* Enhancement - Enable the Cart and Checkout blocks when WooCommerce Blocks is bundled in WooCommerce Core. ([6805](https://github.com/woocommerce/woocommerce-blocks/pull/6805)) * Fix - Refactor Product Categories block to use block.json.([6875](https://github.com/woocommerce/woocommerce-blocks/pull/6875)) * Fix - Add font-weight controls to the Mini Cart block text.([6760](https://github.com/woocommerce/woocommerce-blocks/pull/6760)) * Fix - Fix proceed to checkout button not working for custom links.([6804](https://github.com/woocommerce/woocommerce-blocks/pull/6804)) @@ -269,7 +286,10 @@ * Fix - Fixes an issue where search lists would not preserve the case of the original item.([6551](https://github.com/woocommerce/woocommerce-blocks/pull/6551)) * Fix - Prevent Featured Product block from breaking when product is out of stock + hidden from catalog.([6640](https://github.com/woocommerce/woocommerce-blocks/pull/6640)) * Fix - Contrast improvement for checkout error messages when displayed over a theme's dark mode.([6292](https://github.com/woocommerce/woocommerce-blocks/pull/6292)) - +* Fix - Refactor Product Categories block to use block.json. ([6875](https://github.com/woocommerce/woocommerce-blocks/pull/6875)) +* Fix - Fix: Update billing address when shipping address gets change in shipping calculator at Cart block. ([6823](https://github.com/woocommerce/woocommerce-blocks/pull/6823)) +* Fix - Fix: Add font-weight controls to the Mini Cart block text. ([6760](https://github.com/woocommerce/woocommerce-blocks/pull/6760)) +* Fix - Prevent unnecessarily showing the item names in a shipping package if it's the only package. ([6899](https://github.com/woocommerce/woocommerce-blocks/pull/6899)) = 6.8.0 2022-08-09 = **WooCommerce** @@ -321,9 +341,9 @@ * Update - Implement bulk actions in the new orders admin list table. [#33687](https://github.com/woocommerce/woocommerce/pull/33687) * Update - Making default state of product image meta boxes more prominent. [#33707](https://github.com/woocommerce/woocommerce/pull/33707) * Update - Randomize the order of sections in Recommended Marketing Extensions [#33851](https://github.com/woocommerce/woocommerce/pull/33851) -* Update - Removed two-col task list experiments code [#33643](https://github.com/woocommerce/woocommerce/pull/33643) +* Update - Removed two-col task list expierments code [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Update - Remove legacy image sizes [#33772](https://github.com/woocommerce/woocommerce/pull/33772) -* Update - Set default shipping methods when store country is the US and Jetpack is installed [#33788](https://github.com/woocommerce/woocommerce/pull/33788) +* Update - Set ddefault shipping methods when store country is the US and Jetpack is intalled [#33788](https://github.com/woocommerce/woocommerce/pull/33788) * Update - Set smart shipping feature flags to true [#33819](https://github.com/woocommerce/woocommerce/pull/33819) * Update - Update display shipping task logic and add ReviewShippingOptions task [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Update - Update review shipping options task complete logic [#33650](https://github.com/woocommerce/woocommerce/pull/33650) @@ -332,7 +352,7 @@ * Update - Update wcpay suggestion UI in payment task [#33717](https://github.com/woocommerce/woocommerce/pull/33717) * Update - Update wcpay to include a mention of in-person payments for Canada [#33669](https://github.com/woocommerce/woocommerce/pull/33669) * Update - Update WooCommerce Blocks to 8.0.0 [#33736](https://github.com/woocommerce/woocommerce/pull/33736) -* Update - Uses WC_Data_Store directly to count the shipping zones to avoid any unnecessary query to the D.B [#33643](https://github.com/woocommerce/woocommerce/pull/33643) +* Update - Uses WC_Data_Store directly to count the shipping zones to avoid any unncessary query to the D.B [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Dev - Add playwright e2e README.md [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Dev - Add `$transaction_id` as arg to various `payment_complete` hooks. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Dev - Add `wc com extension install` CLI command [#33775](https://github.com/woocommerce/woocommerce/pull/33775) @@ -405,10 +425,10 @@ * Fix - Fix broken design of Single Product template in block themes when product had no reviews or additional info [#33329](https://github.com/woocommerce/woocommerce/pull/33329) * Fix - Clean up unused remote inbox notifications option [#33373](https://github.com/woocommerce/woocommerce/pull/33373) * Fix - Settings: fix Tracks event when enabling/disabling advanced features settings. [#33305](https://github.com/woocommerce/woocommerce/pull/33305) -* Fix - Fix accidentally deleted method during merge for 33034. [#33142](https://github.com/woocommerce/woocommerce/pull/33142) +* Fix - Fix accidently deleted method during merge for 33034. [#33142](https://github.com/woocommerce/woocommerce/pull/33142) * Fix - Fix the styling of the frame in the Leaderboard section of Analytics. [#33163](https://github.com/woocommerce/woocommerce/pull/33163) * Fix - Fix obw free extension rules for the marketing task with php 8 [#33329](https://github.com/woocommerce/woocommerce/pull/33329) -* Fix - Fix product tour spotlight location [#33329](https://github.com/woocommerce/woocommerce/pull/33329) +* Fix - Fix product tour splotlight location [#33329](https://github.com/woocommerce/woocommerce/pull/33329) * Fix - Fix product tour TypeError when reading innerHTML [#33448](https://github.com/woocommerce/woocommerce/pull/33448) * Fix - Fix: Content width issue and Classic Template blocks alignment issue for Twenty Twenty-Two. [#33329](https://github.com/woocommerce/woocommerce/pull/33329) * Fix - Fix typos in various cot migration messages. [#33329](https://github.com/woocommerce/woocommerce/pull/33329) @@ -528,7 +548,7 @@ * Dev - Remove the post_id column from the orders table, and adjust the SQL queries that count/get out of sync orders accordingly. [#32706](https://github.com/woocommerce/woocommerce/pull/32706) * Dev - Update woo admin ts config to have an isolated TS environment [#32616](https://github.com/woocommerce/woocommerce/pull/32616) * Dev - Updating scripts to use pnpm/Nx commands [#32943](https://github.com/woocommerce/woocommerce/pull/32943) -* Dev - Remove woo tracks type declaration from woo admin ./client. [#32937](https://github.com/woocommerce/woocommerce/pull/32937) +* Dev - Remove woo tracks type declaration from woo admin ./cleint. [#32937](https://github.com/woocommerce/woocommerce/pull/32937) * Dev - Fix react admin ./client type errors after updating @woocommerce/data types [#32735](https://github.com/woocommerce/woocommerce/pull/32735) * Dev - Removed temporary codepath added in #32603 since translation paths have been updated [#33226](https://github.com/woocommerce/woocommerce/pull/33226) * Enhancement - Add fallback image for payments task gateway icons [#32773](https://github.com/woocommerce/woocommerce/pull/32773) @@ -573,12 +593,12 @@ * Performance - Fix system status API requests that only query some fields [#32823](https://github.com/woocommerce/woocommerce/pull/32823) * Tweak - For Vietnam, the second street address line should be displayed but not required. [#32610](https://github.com/woocommerce/woocommerce/pull/32610) * Tweak - Comment: We're adding extra protections to a newly introduced feature; a further changelog entry is not needed. [#32771](https://github.com/woocommerce/woocommerce/pull/32771) -* Tweak - Fix spacing between the payment logo assets in the payment banner experiment. [#33065](https://github.com/woocommerce/woocommerce/pull/33065) +* Tweak - Fix spacing between the Paymetn logo assets in the payment banner experiment. [#33065](https://github.com/woocommerce/woocommerce/pull/33065) * Tweak - Comment: Omitting a changelog entry, because we're correcting an unreleased oversight. [#32744](https://github.com/woocommerce/woocommerce/pull/32744) * Tweak - Update TikTok onboarding icon [#32857](https://github.com/woocommerce/woocommerce/pull/32857) * Tweak - Fix typescript type errors in react admin ./client/shipping [#32688](https://github.com/woocommerce/woocommerce/pull/32688) * Tweak - Comment: Improves a newly added feature, so a further changelog entry is not required. [#32776](https://github.com/woocommerce/woocommerce/pull/32776) -* Tweak - Add wc-admin-deactivate-plugin to list of obsolete notes so it gets deleted on upgrade [#32982](https://github.com/woocommerce/woocommerce/pull/32982) +* Tweak - Add wc-admin-deactivate-plugin to list of obselete notes so it gets deleted on upgrade [#32982](https://github.com/woocommerce/woocommerce/pull/32982) * Tweak - Fix typescript type errors in react admin ./client/wp-admin-scripts [#32678](https://github.com/woocommerce/woocommerce/pull/32678) * Tweak - Move the file for the DatabaseUtil class to the proper directory according to its namespace. [#33109](https://github.com/woocommerce/woocommerce/pull/33109) * Tweak - Also allow getting category ID as option ID instead of term slug in wc-enhanced-select. [#32743](https://github.com/woocommerce/woocommerce/pull/32743) @@ -662,7 +682,7 @@ * Add - a new `woocommerce_generate_{$type}_html` action hook to generate custom field types in `WC_Settings_API` class objects. ([#31238](https://github.com/woocommerce/woocommerce/pull/31238)) * Add - Make the `$webhook` object available to consumers of the `woocommerce_webhook_options` action. ([#31292](https://github.com/woocommerce/woocommerce/pull/31292)) * Add - Pinterest extension to onboarding wizard and marketing task ([#32527](https://github.com/woocommerce/woocommerce/pull/32527)) -* Add - `order_item_display_meta` option to orders endpoint (REST API), to support filtering out variation meta. +* Add - `order_item_display_meta` option to orders endpoint (REST API), to osupport filtering out variation meta. * Add - new hooks to `order-tracking.php` form. ([#30320](https://github.com/woocommerce/woocommerce/pull/30320)) * Fix - Ensure that an existing order with auto-draft status won't be interpreted as pending when determining if the status has changed. ([#32571](https://github.com/woocommerce/woocommerce/pull/32571)) * Fix - bug in which tasks reminder bar was displayed on product screens. ([#32526](https://github.com/woocommerce/woocommerce/pull/32526)) @@ -679,7 +699,7 @@ * Fix - WCPay task add missing legal message within task. ([#32762](https://github.com/woocommerce/woocommerce/issues/32762)) * Tweak - Make it possible for downloadable files to be in an enabled or disabled state. * Tweak - UI changes for set up payments task -* Tweak - Update WCA deactivation hooks to work with WC deactivation. +* Tweak - Update WCA deactivation hooks to work with WC deactvation. * Tweak - Move feature flag config files to Woocommerce plugin to support unit test execution in the wp-env environment. * Tweak - Update progress header bar styles in task list ([#32498](https://github.com/woocommerce/woocommerce/pull/32498)) * Tweak - Update country strings, rename Swaziland to Eswatini (per CLDR R41 update). ([#31185](https://github.com/woocommerce/woocommerce/pull/31185)) @@ -692,7 +712,7 @@ * Dev - Merge WCA install routines to the core * Dev - Remove `load_plugin_textdomain` method from admin plugin. * Dev - Simplify the WooCommerce Admin init routine. ([#32489](https://github.com/woocommerce/woocommerce/pull/32489)) -* Dev - Generic migration support for migration from posts + postsmeta table to any custom table. Additionally, implement migrations to various COT tables using this generic support. +* Dev - Generic migration support for migration from posts + postsmeta table to any custom table. Additionaly, implement migrations to various COT tables using this generic support. * Dev - Remove Pinterest extension from OBW ([#32626](https://github.com/woocommerce/woocommerce/pull/32626)) * Dev - Revert back menu position to floats as string for WP compatibility. * Dev - Enable the "Save changes" button within the variations panel when a textfield receives input. ([#32589](https://github.com/woocommerce/woocommerce/pull/32589)) @@ -821,7 +841,7 @@ * Enhancement - Add support for the global style for the On-Sale Badge block. ([5565](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/*565)) * Enhancement - Add support for the global style for the Attribute Filter block. ([5557](https://github.com/woocommerce/woocommerce-gutenberg-products-block/*ull/5557)) * Enhancement - Category List block: Add support for global style. ([5516](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5516)) -* Fix - Fixed typo in `woocommerce_store_api_validate_add_to_cart` and `woocommerce_store_api_validate_cart_item` hook names. ([5926](https://github.com/*oocommerce/woocommerce-gutenberg-products-block/pull/5926)) +* Fix - Fixed typo in `wooocommerce_store_api_validate_add_to_cart` and `wooocommerce_store_api_validate_cart_item` hook names. ([5926](https://github.com/*oocommerce/woocommerce-gutenberg-products-block/pull/5926)) * Fix - Fix loading WC core translations in locales where WC Blocks is not localized for some strings. ([5910](https://github.com/woocommerce/*oocommerce-gutenberg-products-block/pull/5910)) * Fix - Fixed an issue where clear customizations functionality was not working for WooCommerce templates. ([5746](https://github.com/woocommerce/*oocommerce-gutenberg-products-block/pull/5746)) * Fix - Fixed hover and focus states for button components. ([5712](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5712)) @@ -1113,7 +1133,7 @@ * Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). ( [#761](https://github.com/woocommerce/action-scheduler/pull/761) ) * Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). ( [#768](https://github.com/woocommerce/action-scheduler/pull/768) ) * Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). ( [#762](https://github.com/woocommerce/action-scheduler/pull/762) ) -* Dev - Improve actions table indices (props @glagonikas). ( [#774](https://github.com/woocommerce/action-scheduler/pull/774) & [#777](https://github.com/woocommerce/action-scheduler/pull/777) ) +* Dev - Improve actions table indicies (props @glagonikas). ( [#774](https://github.com/woocommerce/action-scheduler/pull/774) & [#777](https://github.com/woocommerce/action-scheduler/pull/777) ) * Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). ( [#769](https://github.com/woocommerce/action-scheduler/pull/769) & [#778](https://github.com/woocommerce/action-scheduler/pull/778) ) * Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). ( [#763](https://github.com/woocommerce/action-scheduler/pull/763) & [#779](https://github.com/woocommerce/action-scheduler/pull/779) ) * Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). ( [#773](https://github.com/woocommerce/action-scheduler/pull/773) ) @@ -1234,7 +1254,7 @@ * Fix - Ensure homescreen defaults to single column layout. ( [#7969](https://github.com/woocommerce/woocommerce-admin/issues/7969) ) * Fix: Fix shipping task not offering step 3. ( [#7985](https://github.com/woocommerce/woocommerce-admin/issues/7985) ) * Add - Add Avalara to tax task ( [#7874](https://github.com/woocommerce/woocommerce-admin/issues/7874) ) -* Add - Add 2col experiment. ( [#7872](https://github.com/woocommerce/woocommerce-admin/issues/7872) ) +* Add - Add 2col expirement. ( [#7872](https://github.com/woocommerce/woocommerce-admin/issues/7872) ) * Add - Added two column experimental task list ( [#7669](https://github.com/woocommerce/woocommerce-admin/issues/7669) ) * Add - Add header cards for all tasks in Tasklist UI experiment ( [#7838](https://github.com/woocommerce/woocommerce-admin/issues/7838) ) * Add - Add onboarding task docs ( [#7762](https://github.com/woocommerce/woocommerce-admin/issues/7762) ) @@ -1656,7 +1676,7 @@ = 5.5.2 2021-07-22 = * Fix - Add a new option allowing product downloads to be served using redirects as a last resort. #30288 -* Fix - Remove unnecessary search related 'where' clause added in the 'post_clauses' hook handling. #30335 +* Fix - Remove unnecessary seacrh related 'where' clause added in the 'post_clauses' hook handling. #30335 * Fix - Check before calling $screen method to make sure its not null. #30277 **WooCommerce Admin - 2.4.4 & 2.4.3 & 2.4.2 ** @@ -1761,7 +1781,7 @@ * Fix - RemoteFreeExtension hide bundle when all of its plugins are not visible #7182 * Fix - Issue where summary stats were not showing in Analytics > Stock. #7161 * Fix - Rule Processing Transformer to handle dotNotation default value #7009 -* Fix - Remove Navigation's unneeded SlotFill context #6832 +* Fix - Remove Navigation's uneeded SlotFill context #6832 * Fix - Report filters expecting specific ordering. #6847 * Fix - Render bug with report comparison mode selections. #6862 * Fix - Throw exception if the data store cannot be loaded when trying to use notes. #6771 @@ -1917,7 +1937,7 @@ * Fix - Make pagination buttons height and width consistent #6725 * Fix - Retain persisted queries when navigating to Homescreen #6614 * Fix - Update folded header style #6724 -* Fix - Unrelated variations showing up in the Products reports #6647 +* Fix - Unreleated variations showing up in the Products reports #6647 * Fix - Check active plugins before getting the PayPal onboarding status #6625 * Fix - Remove no-reply from inbox notification emails #6644 * Fix - Set up shipping costs task, redirect to shipping settings after completion. #6791 @@ -2340,7 +2360,7 @@ * Fix - Onboarding - Fixed "Business Details" error. #6271 * Fix - Show management links when only main task list is hidden. #6291 * Fix - Correct the Klarna slug. #6440 -* Add - new inbox message - Getting started in e-commerce - watch this webinar. #6086 +* Add - new inbox message - Getting started in Ecommerce - watch this webinar. #6086 * Add - Remote inbox notifications contains comparison and fix product rule. #6073 * Add - Task list payments - include Mollie as an option. #6257 * Update - store deprecation welcome modal support doc link #6094 @@ -2367,7 +2387,7 @@ * Enhancements - Fix: Added fallback styling for screen reader text. ([3557](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3557)) * Fix - Ensure empty categories are correctly hidden in the product categories block. ([3765](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3765)) * Fix - Added missing wrapper div within FeaturedCategory and FeatureProduct blocks. ([3746](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3746)) -* Fix - Set correct text color in BlockErrorBoundary notices. ([3738](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3738)) +* Fix - Set correct text color in BlockErrorBoundry notices. ([3738](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3738)) * Fix - Hidden cart item meta data will not be rendered in the Cart and Checkout blocks. ([3732](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3732)) * Fix - Improved accessibility of product image links in the products block by using correct aria tags and hiding empty image placeholders. ([3722](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3722)) * Fix - Add missing aria-label for stars image in the review-list-item component. ([3706](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3706)) @@ -2785,7 +2805,7 @@ * 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 built-in round function in PHP8. #27830 +* 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. @@ -2898,7 +2918,7 @@ - Dev: Store Profiler - Industry step: reduced padding and removed industry #5157 - Dev: Remove product settings video note #5213 - Enhancement: Add free local shipping zone on profile complete #4857 -- Enhancement: Add woocommerce/tracks package #5107 +- Enhancement: Add woocommerce/tracks pacakage #5107 - Enhancement: Add filter to allow modification of report columns #4984 - Enhancement: Add WooCommerce Mobile Banner #5037 - Enhancement: Add Product Attribute advanced filter #5038 @@ -2972,7 +2992,7 @@ * Localization - Added postcode validation for Bosnia and Herzegovina. #27048 * Localization - Added the postcode validation for Liechtenstein. #27059 * Localization - Add i18n locale information for Liechtenstein, Switzerland and Austria. #27193 -* Tweak - Increase priority of `admin_body_class` filter to avoid conflict with plugins that incorrectly remove all body classes from WP. #27426 +* Tweak - Increase priority of `admin_body_class` filter to avoid comflict with plugins that incorrectly remove all body classes from WP. #27426 * Tweak - Rename built-in PayPal payment method to PayPal Standard. #27468 * Fix - Remove whitespace within a link. #26897 * Fix - `get_review_count_for_product` return all comments count not only 'review' types #26928 @@ -3083,7 +3103,7 @@ * Fix - After clicking to update WooCommerce, the user will stay in the same page instead of being redirected to the "Settings" page. #27172 * Fix - "Product type" dropdown missing from Product's data meta box on WP 5.5. #27170 * Fix - Removed the JETPACK_AUTOLOAD_DEV define. #27185 -* Fix - Fixed "virtual" and "downloadable" pointers on product walkthrough. #27145 +* Fix - Fixed "virtual" and "downlodable" pointers on product walkthrough. #27145 * Fix - Updated tested up to for WordPress 5.5. #27334 * Dev - Update WooCommerce Admin version to v1.4.0. #27378 * Dev - Upgraded to v2.2 of Jetpack Autoloader. #27358 @@ -3160,7 +3180,7 @@ * Performance - Don't load shortcode Cart and Checkout scripts when using the blocks. #2842 * Performance - Scripts only relevant to the frontend side of blocks are no longer loaded in the editor. #2788 * Performance - Lazy Loading Atomic Components. #2777 -* Refactor - Remove dashicon classes. #2848 +* Pefactor - Remove dashicon classes. #2848 **WooCommerce Blocks 3.1.0** * Fix - Missing permissions_callback arg in StoreApi route definitions. #2926 @@ -3250,7 +3270,7 @@ * Enhancement - Add API tool to verify base DB tables. woocommerce/woocommerce-rest-api#188 **WooCommerce Admin 1.3.0** -* Enhancement - Add Jetpack stats to performance indicators / homepage #4291 +* Enhancement - Add Jetpack stats to performance indicatorts / homepage #4291 * Enhancement - New "Store Management" quick links card on WooCommerce home screen. #4350 * Enhancement - Inbox notifications layout updates #4218 * Enhancement - New Home Screen #4303 @@ -3518,7 +3538,7 @@ * Dev - Made the default test source folders support the system tmp folder. #25923 * Dev - Add cart & checkout block/shortcode info to tracker data. #25932 * Dev - Make WC_Product_Data_Store_CPT::update_product_stock operations atomic. #26039 -* Dev - Adds usage data for the of cart & checkout blocks (currently in development in WooCommerce Blocks plugin) to the WC Tracker snapshot. #26084 +* Dev - Adds usage data for the of cart & checkout blocks (currently in development in WooCommmerce Blocks plugin) to the WC Tracker snapshot. #26084 * Dev - Implement some additional tracks for coupons, orders, and products. #26085 = 4.0.4 2022-03-10 = @@ -3696,7 +3716,7 @@ * Tweak - Cache checkout fragments and update DOM on change only. #24227 * Tweak - Eliminate extra update order AJAX request on checkout page load. #24271 * Tweak - Prevent billing address from being updated on shipping update. #24374 -* Tweak - Added a tooltip in the "Coupon expiry date" field. #24749 +* Tweak - Added a tooltip in the "Coupon expity date" field. #24749 * Tweak - Make phone numbers clickable in emails. #24786 * Tweak - Prevent PHP warnings in tracker if order doesn't have a created date yet. #24846 * Tweak - Capitalize "T" in "Move to Trash" phrase on order page in wp-admin to be consistent with product and coupon pages. #24867 @@ -3802,7 +3822,7 @@ * Tweak - Prevent filter per category while exporting product variations. #24517 * Tweak - Better wording for subtotal of items in cart and review order. #24440 * Tweak - Prevent new lines in product quantity in checkout details. #24311 -* Tweak - Add a tooltip in the "Coupon expiry date" field. #24749 +* Tweak - Add a tooltip in the "Coupon expity date" field. #24749 * Tweak - CSS styling changes for WP 5.3. #24832 * Template - Moved HTML for displaying product price filter widget to a new template `product price filter widget`. #23384 * Accessibility - Make $subtext color darker. #24739 From 076f6f28390decadff5f329a4134cec44e45db1a Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 4 Nov 2022 15:29:25 +0100 Subject: [PATCH 143/149] Add missing Ukrainian Subdivisions to States.php (#35493) Co-authored-by: Corey Gehrke --- plugins/woocommerce/changelog/pr-34834 | 4 + plugins/woocommerce/i18n/states.php | 53 +-- .../tests/data/data-crud.test.js | 318 ++++++++++-------- 3 files changed, 209 insertions(+), 166 deletions(-) create mode 100644 plugins/woocommerce/changelog/pr-34834 diff --git a/plugins/woocommerce/changelog/pr-34834 b/plugins/woocommerce/changelog/pr-34834 new file mode 100644 index 00000000000..8e62b02db29 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-34834 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Added Ukrainian subdivisions. diff --git a/plugins/woocommerce/i18n/states.php b/plugins/woocommerce/i18n/states.php index dfe5a790fed..b43d34cd698 100644 --- a/plugins/woocommerce/i18n/states.php +++ b/plugins/woocommerce/i18n/states.php @@ -1808,31 +1808,34 @@ return array( 'RSVO' => _x( 'Vojvodina', 'district', 'woocommerce' ), ), 'SE' => array(), - 'UA' => array( // Ukrainian oblasts. - 'VN' => __( 'Vinnytsia Oblast', 'woocommerce' ), - 'VL' => __( 'Volyn Oblast', 'woocommerce' ), - 'DP' => __( 'Dnipropetrovsk Oblast', 'woocommerce' ), - 'DT' => __( 'Donetsk Oblast', 'woocommerce' ), - 'ZT' => __( 'Zhytomyr Oblast', 'woocommerce' ), - 'ZK' => __( 'Zakarpattia Oblast', 'woocommerce' ), - 'ZP' => __( 'Zaporizhzhia Oblast', 'woocommerce' ), - 'IF' => __( 'Ivano-Frankivsk Oblast', 'woocommerce' ), - 'KV' => __( 'Kyiv Oblast', 'woocommerce' ), - 'KH' => __( 'Kirovohrad Oblast', 'woocommerce' ), - 'LH' => __( 'Luhansk Oblast', 'woocommerce' ), - 'LV' => __( 'Lviv Oblast', 'woocommerce' ), - 'MY' => __( 'Mykolaiv Oblast', 'woocommerce' ), - 'OD' => __( 'Odessa Oblast', 'woocommerce' ), - 'PL' => __( 'Poltava Oblast', 'woocommerce' ), - 'RV' => __( 'Rivne Oblast', 'woocommerce' ), - 'SM' => __( 'Sumy Oblast', 'woocommerce' ), - 'TP' => __( 'Ternopil Oblast', 'woocommerce' ), - 'KK' => __( 'Kharkiv Oblast', 'woocommerce' ), - 'KS' => __( 'Kherson Oblast', 'woocommerce' ), - 'KM' => __( 'Khmelnytskyi Oblast', 'woocommerce' ), - 'CK' => __( 'Cherkasy Oblast', 'woocommerce' ), - 'CH' => __( 'Chernihiv Oblast', 'woocommerce' ), - 'CV' => __( 'Chernivtsi Oblast', 'woocommerce' ), + 'UA' => array( // Ukrainian oblasts. https://github.com/unicode-org/cldr/blob/release-42/common/subdivisions/en.xml#L5243. + 'UA05' => __( 'Vinnychchyna', 'woocommerce' ), + 'UA07' => __( 'Volyn', 'woocommerce' ), + 'UA09' => __( 'Luhanshchyna', 'woocommerce' ), + 'UA12' => __( 'Dnipropetrovshchyna', 'woocommerce' ), + 'UA14' => __( 'Donechchyna', 'woocommerce' ), + 'UA18' => __( 'Zhytomyrshchyna', 'woocommerce' ), + 'UA21' => __( 'Zakarpattia', 'woocommerce' ), + 'UA23' => __( 'Zaporizhzhya', 'woocommerce' ), + 'UA26' => __( 'Prykarpattia', 'woocommerce' ), + 'UA30' => __( 'Kyiv', 'woocommerce' ), + 'UA32' => __( 'Kyivshchyna', 'woocommerce' ), + 'UA35' => __( 'Kirovohradschyna', 'woocommerce' ), + 'UA40' => __( 'Sevastopol', 'woocommerce' ), + 'UA43' => __( 'Crimea', 'woocommerce' ), + 'UA46' => __( 'Lvivshchyna', 'woocommerce' ), + 'UA48' => __( 'Mykolayivschyna', 'woocommerce' ), + 'UA51' => __( 'Odeshchyna', 'woocommerce' ), + 'UA53' => __( 'Poltavshchyna', 'woocommerce' ), + 'UA56' => __( 'Rivnenshchyna', 'woocommerce' ), + 'UA59' => __( 'Sumshchyna', 'woocommerce' ), + 'UA61' => __( 'Ternopilshchyna', 'woocommerce' ), + 'UA63' => __( 'Kharkivshchyna', 'woocommerce' ), + 'UA65' => __( 'Khersonshchyna', 'woocommerce' ), + 'UA68' => __( 'Khmelnychchyna', 'woocommerce' ), + 'UA71' => __( 'Cherkashchyna', 'woocommerce' ), + 'UA74' => __( 'Chernihivshchyna', 'woocommerce' ), + 'UA77' => __( 'Chernivtsi Oblast', 'woocommerce' ), ), 'UG' => array( // Ugandan districts. 'UG314' => __( 'Abim', 'woocommerce' ), diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js index 824c248d6bb..fd75cca644b 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js @@ -7612,99 +7612,111 @@ test.describe('Data API tests', () => { "thousand_sep": " ", "weight_unit": "kg", "states": [{ - "code": "VN", - "name": "Vinnytsia Oblast" + "code": "UA05", + "name": "Vinnychchyna" }, { - "code": "VL", - "name": "Volyn Oblast" + "code": "UA07", + "name": "Volyn" }, { - "code": "DP", - "name": "Dnipropetrovsk Oblast" + "code": "UA09", + "name": "Luhanshchyna" }, { - "code": "DT", - "name": "Donetsk Oblast" + "code": "UA12", + "name": "Dnipropetrovshchyna" }, { - "code": "ZT", - "name": "Zhytomyr Oblast" + "code": "UA14", + "name": "Donechchyna" }, { - "code": "ZK", - "name": "Zakarpattia Oblast" + "code": "UA18", + "name": "Zhytomyrshchyna" }, { - "code": "ZP", - "name": "Zaporizhzhia Oblast" + "code": "UA21", + "name": "Zakarpattia" }, { - "code": "IF", - "name": "Ivano-Frankivsk Oblast" + "code": "UA23", + "name": "Zaporizhzhya" }, { - "code": "KV", - "name": "Kyiv Oblast" + "code": "UA26", + "name": "Prykarpattia" }, { - "code": "KH", - "name": "Kirovohrad Oblast" + "code": "UA30", + "name": "Kyiv" }, { - "code": "LH", - "name": "Luhansk Oblast" + "code": "UA32", + "name": "Kyivshchyna" }, { - "code": "LV", - "name": "Lviv Oblast" + "code": "UA35", + "name": "Kirovohradschyna" }, { - "code": "MY", - "name": "Mykolaiv Oblast" + "code": "UA40", + "name": "Sevastopol" }, { - "code": "OD", - "name": "Odessa Oblast" + "code": "UA43", + "name": "Crimea" }, { - "code": "PL", - "name": "Poltava Oblast" + "code": "UA46", + "name": "Lvivshchyna" }, { - "code": "RV", - "name": "Rivne Oblast" + "code": "UA48", + "name": "Mykolayivschyna" }, { - "code": "SM", - "name": "Sumy Oblast" + "code": "UA51", + "name": "Odeshchyna" }, { - "code": "TP", - "name": "Ternopil Oblast" + "code": "UA53", + "name": "Poltavshchyna" }, { - "code": "KK", - "name": "Kharkiv Oblast" + "code": "UA56", + "name": "Rivnenshchyna" }, { - "code": "KS", - "name": "Kherson Oblast" + "code": "UA59", + "name": "Sumshchyna" }, { - "code": "KM", - "name": "Khmelnytskyi Oblast" + "code": "UA61", + "name": "Ternopilshchyna" }, { - "code": "CK", - "name": "Cherkasy Oblast" + "code": "UA63", + "name": "Kharkivshchyna" }, { - "code": "CH", - "name": "Chernihiv Oblast" + "code": "UA65", + "name": "Khersonshchyna" }, { - "code": "CV", + "code": "UA68", + "name": "Khmelnychchyna" + }, + { + "code": "UA71", + "name": "Cherkashchyna" + }, + { + "code": "UA74", + "name": "Chernihivshchyna" + }, + { + "code": "UA77", "name": "Chernivtsi Oblast" } ] @@ -13307,99 +13319,111 @@ test.describe('Data API tests', () => { "thousand_sep": " ", "weight_unit": "kg", "states": [{ - "code": "VN", - "name": "Vinnytsia Oblast" + "code": "UA05", + "name": "Vinnychchyna" + }, + { + "code": "UA07", + "name": "Volyn" }, { - "code": "VL", - "name": "Volyn Oblast" + "code": "UA09", + "name": "Luhanshchyna" }, { - "code": "DP", - "name": "Dnipropetrovsk Oblast" + "code": "UA12", + "name": "Dnipropetrovshchyna" }, { - "code": "DT", - "name": "Donetsk Oblast" + "code": "UA14", + "name": "Donechchyna" }, { - "code": "ZT", - "name": "Zhytomyr Oblast" + "code": "UA18", + "name": "Zhytomyrshchyna" }, { - "code": "ZK", - "name": "Zakarpattia Oblast" + "code": "UA21", + "name": "Zakarpattia" }, { - "code": "ZP", - "name": "Zaporizhzhia Oblast" + "code": "UA23", + "name": "Zaporizhzhya" }, { - "code": "IF", - "name": "Ivano-Frankivsk Oblast" + "code": "UA26", + "name": "Prykarpattia" }, { - "code": "KV", - "name": "Kyiv Oblast" + "code": "UA30", + "name": "Kyiv" }, { - "code": "KH", - "name": "Kirovohrad Oblast" + "code": "UA32", + "name": "Kyivshchyna" }, { - "code": "LH", - "name": "Luhansk Oblast" + "code": "UA35", + "name": "Kirovohradschyna" }, { - "code": "LV", - "name": "Lviv Oblast" + "code": "UA40", + "name": "Sevastopol" }, { - "code": "MY", - "name": "Mykolaiv Oblast" + "code": "UA43", + "name": "Crimea" }, { - "code": "OD", - "name": "Odessa Oblast" + "code": "UA46", + "name": "Lvivshchyna" }, { - "code": "PL", - "name": "Poltava Oblast" + "code": "UA48", + "name": "Mykolayivschyna" }, { - "code": "RV", - "name": "Rivne Oblast" + "code": "UA51", + "name": "Odeshchyna" }, { - "code": "SM", - "name": "Sumy Oblast" + "code": "UA53", + "name": "Poltavshchyna" }, { - "code": "TP", - "name": "Ternopil Oblast" + "code": "UA56", + "name": "Rivnenshchyna" }, { - "code": "KK", - "name": "Kharkiv Oblast" + "code": "UA59", + "name": "Sumshchyna" }, { - "code": "KS", - "name": "Kherson Oblast" + "code": "UA61", + "name": "Ternopilshchyna" }, { - "code": "KM", - "name": "Khmelnytskyi Oblast" + "code": "UA63", + "name": "Kharkivshchyna" }, { - "code": "CK", - "name": "Cherkasy Oblast" + "code": "UA65", + "name": "Khersonshchyna" }, { - "code": "CH", - "name": "Chernihiv Oblast" + "code": "UA68", + "name": "Khmelnychchyna" }, { - "code": "CV", + "code": "UA71", + "name": "Cherkashchyna" + }, + { + "code": "UA74", + "name": "Chernihivshchyna" + }, + { + "code": "UA77", "name": "Chernivtsi Oblast" } ] @@ -23992,99 +24016,111 @@ test.describe('Data API tests', () => { "code": "UA", "name": "Ukraine", "states": [{ - "code": "VN", - "name": "Vinnytsia Oblast" + "code": "UA05", + "name": "Vinnychchyna" + }, + { + "code": "UA07", + "name": "Volyn" }, { - "code": "VL", - "name": "Volyn Oblast" + "code": "UA09", + "name": "Luhanshchyna" }, { - "code": "DP", - "name": "Dnipropetrovsk Oblast" + "code": "UA12", + "name": "Dnipropetrovshchyna" }, { - "code": "DT", - "name": "Donetsk Oblast" + "code": "UA14", + "name": "Donechchyna" }, { - "code": "ZT", - "name": "Zhytomyr Oblast" + "code": "UA18", + "name": "Zhytomyrshchyna" }, { - "code": "ZK", - "name": "Zakarpattia Oblast" + "code": "UA21", + "name": "Zakarpattia" }, { - "code": "ZP", - "name": "Zaporizhzhia Oblast" + "code": "UA23", + "name": "Zaporizhzhya" }, { - "code": "IF", - "name": "Ivano-Frankivsk Oblast" + "code": "UA26", + "name": "Prykarpattia" }, { - "code": "KV", - "name": "Kyiv Oblast" + "code": "UA30", + "name": "Kyiv" }, { - "code": "KH", - "name": "Kirovohrad Oblast" + "code": "UA32", + "name": "Kyivshchyna" }, { - "code": "LH", - "name": "Luhansk Oblast" + "code": "UA35", + "name": "Kirovohradschyna" }, { - "code": "LV", - "name": "Lviv Oblast" + "code": "UA40", + "name": "Sevastopol" }, { - "code": "MY", - "name": "Mykolaiv Oblast" + "code": "UA43", + "name": "Crimea" }, { - "code": "OD", - "name": "Odessa Oblast" + "code": "UA46", + "name": "Lvivshchyna" }, { - "code": "PL", - "name": "Poltava Oblast" + "code": "UA48", + "name": "Mykolayivschyna" }, { - "code": "RV", - "name": "Rivne Oblast" + "code": "UA51", + "name": "Odeshchyna" }, { - "code": "SM", - "name": "Sumy Oblast" + "code": "UA53", + "name": "Poltavshchyna" }, { - "code": "TP", - "name": "Ternopil Oblast" + "code": "UA56", + "name": "Rivnenshchyna" }, { - "code": "KK", - "name": "Kharkiv Oblast" + "code": "UA59", + "name": "Sumshchyna" }, { - "code": "KS", - "name": "Kherson Oblast" + "code": "UA61", + "name": "Ternopilshchyna" }, { - "code": "KM", - "name": "Khmelnytskyi Oblast" + "code": "UA63", + "name": "Kharkivshchyna" }, { - "code": "CK", - "name": "Cherkasy Oblast" + "code": "UA65", + "name": "Khersonshchyna" }, { - "code": "CH", - "name": "Chernihiv Oblast" + "code": "UA68", + "name": "Khmelnychchyna" }, { - "code": "CV", + "code": "UA71", + "name": "Cherkashchyna" + }, + { + "code": "UA74", + "name": "Chernihivshchyna" + }, + { + "code": "UA77", "name": "Chernivtsi Oblast" } ], From ce133089b90306900711055e9c81f17d3f61941f Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Fri, 4 Nov 2022 09:11:26 -0700 Subject: [PATCH 144/149] Fix random failing changelog entry (#35425) --- .github/workflows/cherry-pick.yml | 68 +++++++++++++++---------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 81071f1e5ef..d9a22a4fb85 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -211,45 +211,45 @@ jobs: if ( changelogEntry.match( /comment:/i ) ) { changelogEntry = false; } - } ); - if ( changelogEntry === false ) { - continue; - } - - fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { - if ( err ) { - console.error( err ); + if ( ! changelogEntry ) { + return; } - changelogTxt = data.split( "\n" ); - let isInRange = false; - let newChangelogTxt = []; - - for ( const line of changelogTxt ) { - if ( isInRange === false && line === '== Changelog ==' ) { - isInRange = true; - } - - if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) { - isInRange = false; - } - - // Find the first match of the entry "Type". - if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) { - newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` ); - newChangelogTxt.push( line ); - isInRange = false; - continue; - } - - newChangelogTxt.push( line ); - } - - fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => { + fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { if ( err ) { - console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` ); + console.error( err ); } + + changelogTxt = data.split( "\n" ); + let isInRange = false; + let newChangelogTxt = []; + + for ( const line of changelogTxt ) { + if ( isInRange === false && line === '== Changelog ==' ) { + isInRange = true; + } + + if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) { + isInRange = false; + } + + // Find the first match of the entry "Type". + if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) { + newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` ); + newChangelogTxt.push( line ); + isInRange = false; + continue; + } + + newChangelogTxt.push( line ); + } + + fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => { + if ( err ) { + console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` ); + } + } ); } ); } ); } From 082f318da47b78497f7e2ba754757369ef706499 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Fri, 4 Nov 2022 13:35:19 -0500 Subject: [PATCH 145/149] [HPOS] Add order data store info to SSR (#35487) Add info on order datastore (and sync) to SSR * Add changelog * Make PHPCS happy * Update plugins/woocommerce/includes/admin/views/html-admin-page-status-report.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Remove unnecessary import Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- .../changelog/add-34864-order-data-store-in-ssr | 4 ++++ .../admin/views/html-admin-page-status-report.php | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr diff --git a/plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr b/plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr new file mode 100644 index 00000000000..63ceec99d1b --- /dev/null +++ b/plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Include order datastore information in status report. diff --git a/plugins/woocommerce/includes/admin/views/html-admin-page-status-report.php b/plugins/woocommerce/includes/admin/views/html-admin-page-status-report.php index d0d652a6c07..1d9b77524dc 100644 --- a/plugins/woocommerce/includes/admin/views/html-admin-page-status-report.php +++ b/plugins/woocommerce/includes/admin/views/html-admin-page-status-report.php @@ -7,6 +7,7 @@ use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories; +use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer as Order_DataSynchronizer; defined( 'ABSPATH' ) || exit; @@ -762,6 +763,18 @@ if ( 0 < count( $dropins_mu_plugins['mu_plugins'] ) ) : get( Download_Directories::class )->get_mode() === Download_Directories::MODE_ENABLED ? '' : ''; ?> + + + + get_current_class_name() ); ?> + + get( Automattic\WooCommerce\Internal\Features\FeaturesController::class )->feature_is_enabled( 'custom_order_tables' ) ) : ?> + + + + get( Order_DataSynchronizer::class )->data_sync_is_enabled() ? '' : ''; ?> + + From 254fbd994f152d68aacd0b38ee0e18b522b92834 Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Fri, 4 Nov 2022 11:39:13 -0700 Subject: [PATCH 146/149] Remove post merge comment about adding testing instructions (#35498) --- .github/workflows/pull-request-post-merge-processing.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/pull-request-post-merge-processing.yml b/.github/workflows/pull-request-post-merge-processing.yml index 62705adf07f..9ce002c31b6 100644 --- a/.github/workflows/pull-request-post-merge-processing.yml +++ b/.github/workflows/pull-request-post-merge-processing.yml @@ -37,8 +37,3 @@ jobs: env: PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: "Run the script to post a comment with next steps hint" - run: php add-post-merge-comment.php - env: - PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 17fe37cafe40df624fc0200699744fdbf2bb4b59 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Sat, 5 Nov 2022 22:03:00 -0400 Subject: [PATCH 147/149] Update DateTimePickerControl's popover styling to work with slot-fill (#35343) * Tweak CSS styling of popover to accommodate usage with slot-fill * Story for DateTimePickerControl use with slot-fill * Changelog --- ...-date-time-picker-control-picker-classname | 4 +++ .../date-time-picker-control.scss | 6 ++-- .../date-time-picker-control.tsx | 3 ++ .../stories/index.tsx | 31 +++++++++++++++++-- 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 packages/js/components/changelog/update-date-time-picker-control-picker-classname diff --git a/packages/js/components/changelog/update-date-time-picker-control-picker-classname b/packages/js/components/changelog/update-date-time-picker-control-picker-classname new file mode 100644 index 00000000000..4f444ed8862 --- /dev/null +++ b/packages/js/components/changelog/update-date-time-picker-control-picker-classname @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix DateTimePickerControl's popover styling when slot-fill is used. diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss index 4930b8c7cf8..a3df6290728 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss @@ -5,7 +5,9 @@ margin-right: 8px; } - .components-datetime__date { - border-top: 0; + &__popover { + .components-datetime__date { + border-top: 0; + } } } diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index b133d453ade..42832b6e800 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -280,6 +280,9 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { /> ) } + popoverProps={ { + className: 'woocommerce-date-time-picker-control__popover', + } } renderContent={ () => { const Picker = isDateOnlyPicker ? DatePicker : WpDateTimePicker; const inputDateTime = parseAsLocalDateTime( inputString ); diff --git a/packages/js/components/src/date-time-picker-control/stories/index.tsx b/packages/js/components/src/date-time-picker-control/stories/index.tsx index adcf77f1336..4ce10f12129 100644 --- a/packages/js/components/src/date-time-picker-control/stories/index.tsx +++ b/packages/js/components/src/date-time-picker-control/stories/index.tsx @@ -2,13 +2,13 @@ * External dependencies */ import React from 'react'; -import { Button } from '@wordpress/components'; +import { Button, Popover, SlotFillProvider } from '@wordpress/components'; import { createElement, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { DateTimePickerControl } from '../'; +import { DateTimePickerControl, defaultDateFormat } from '../'; export default { title: 'WooCommerce Admin/components/DateTimePickerControl', @@ -147,3 +147,30 @@ ControlledDateOnlyEndOfDay.args = { timeForDateOnly: 'end-of-day', }; ControlledDateOnlyEndOfDay.decorators = Controlled.decorators; + +function PopoverSlotDecorator( Story, props ) { + return ( +
+ +
+ +
+ +
+
+ ); +} + +export const WithPopoverSlot = Template.bind( {} ); +WithPopoverSlot.args = { + ...Basic.args, + label: 'Start date', + placeholder: 'Enter the start date', + help: 'There is a SlotFillProvider and Popover.Slot on the page', + isDateOnlyPicker: true, +}; +WithPopoverSlot.decorators = [ PopoverSlotDecorator ]; From 4d7185ea11eb4e56b104fe76e11ecf9703b45eba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 11:37:01 -0800 Subject: [PATCH 148/149] Update changelog.txt from release 7.1.0 (#35524) * Update changelog.txt from release 7.1.0 * Add back 7.0.1 logs Co-authored-by: WooCommerce Bot Co-authored-by: roykho --- changelog.txt | 187 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 148 insertions(+), 39 deletions(-) diff --git a/changelog.txt b/changelog.txt index 04e7e45ae80..3c9793b7d46 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,118 @@ == Changelog == += 7.1.0 2022-11-08 = + +**WooCommerce** + +* Fix - Fix business details step when Gutenberg is active [#35448](https://github.com/woocommerce/woocommerce/pull/35448) +* Fix - Check order type is set before returning to prevent notice. [#35349](https://github.com/woocommerce/woocommerce/pull/35349) +* Fix - When HPOS is enabled, posts are authoritative, and sync is enabled, ensure the HPOS record correctly tracks the CPT order record. [#35402](https://github.com/woocommerce/woocommerce/pull/35402) +* Fix - Allow line breaks in order note again. [#35366](https://github.com/woocommerce/woocommerce/pull/35366) +* Fix - Sync orders for stats table. [#35118](https://github.com/woocommerce/woocommerce/pull/35118) +* Fix - Fix (un)trashing of orders when using HPOS [#35125](https://github.com/woocommerce/woocommerce/pull/35125) +* Fix - Check whether order has classname before returning. [#35207](https://github.com/woocommerce/woocommerce/pull/35207) +* Fix - Add billing and shipping address indexes on order update. [#35121](https://github.com/woocommerce/woocommerce/pull/35121) +* Fix - Use correct datastore when backfilling orders. [#35176](https://github.com/woocommerce/woocommerce/pull/35176) +* Fix - (HPOS) Ensure we use GMT when populating the `date_created_gmt` column for orders. [#34875](https://github.com/woocommerce/woocommerce/pull/34875) +* Fix - Admin list table for orders (in HPOS mode) should check in case the user pages beyond the available range. [#34793](https://github.com/woocommerce/woocommerce/pull/34793) +* Fix - Allow features to declare their initial enabled state. [#34867](https://github.com/woocommerce/woocommerce/pull/34867) +* Fix - Customers should be able to pay for orders so long as any required stock reductions have already taken place. [#33575](https://github.com/woocommerce/woocommerce/pull/33575) +* Fix - Do no override order defaults with NULL values (HPOS) [#34822](https://github.com/woocommerce/woocommerce/pull/34822) +* Fix - Fix "Industry" options fails to save in the Industry step after reloading the page for OBW [#34847](https://github.com/woocommerce/woocommerce/pull/34847) +* Fix - Fix a fatal error thrown by init_theorder_object due to the return type declaration [#34730](https://github.com/woocommerce/woocommerce/pull/34730) +* Fix - fixed mismatching jetpack user should not see mobile app task list item [#35052](https://github.com/woocommerce/woocommerce/pull/35052) +* Fix - Fix enable guided mode button not trigger when its text is translated [#34843](https://github.com/woocommerce/woocommerce/pull/34843) +* Fix - Fixes test environment setup setting datetime for customer user creation [#34888](https://github.com/woocommerce/woocommerce/pull/34888) +* Fix - Fix JSON schema for product's image properties. [#34852](https://github.com/woocommerce/woocommerce/pull/34852) +* Fix - Fix obw validation issue to truly disable the continue buttons [#34895](https://github.com/woocommerce/woocommerce/pull/34895) +* Fix - Fix onboarding wizard popover padding for WP6.1 [#34896](https://github.com/woocommerce/woocommerce/pull/34896) +* Fix - Fix order refund removal when the HPOS datastore is in use. [#34785](https://github.com/woocommerce/woocommerce/pull/34785) +* Fix - Handle loading and error states for magic link button [#35068](https://github.com/woocommerce/woocommerce/pull/35068) +* Fix - Implement missing method of calculating shipping and total tax. [#34805](https://github.com/woocommerce/woocommerce/pull/34805) +* Fix - Serialize meta value before rendering so that it's rendered properly. [#34952](https://github.com/woocommerce/woocommerce/pull/34952) +* Fix - Set correct timezone when backfilling data. [#35033](https://github.com/woocommerce/woocommerce/pull/35033) +* Add - Twenty Twenty-Three theme compatibility. [#35306](https://github.com/woocommerce/woocommerce/pull/35306) +* Add - Add handling for plugin-feature incompatibilities [#34879](https://github.com/woocommerce/woocommerce/pull/34879) +* Add - Add inventory stock management to new product management experience [#34984](https://github.com/woocommerce/woocommerce/pull/34984) +* Add - Add new attributes section and field for the new Product Management Experience. [#34751](https://github.com/woocommerce/woocommerce/pull/34751) +* Add - Add order preview functionality to HPOS list table. [#34770](https://github.com/woocommerce/woocommerce/pull/34770) +* Add - Add playwright api-core-tests for customers crud operations [#34945](https://github.com/woocommerce/woocommerce/pull/34945) +* Add - Add playwright api-core-tests for order notes crud operations [#34979](https://github.com/woocommerce/woocommerce/pull/34979) +* Add - Add playwright api-core-tests for product properties crud operations [#34998](https://github.com/woocommerce/woocommerce/pull/34998) +* Add - Add playwright api-core-tests for tax rates crud operations [#34960](https://github.com/woocommerce/woocommerce/pull/34960) +* Add - Add shipping class section and dropdown [#34684](https://github.com/woocommerce/woocommerce/pull/34684) +* Add - Add shipping dimensions section to product page #34329 [#34856](https://github.com/woocommerce/woocommerce/pull/34856) +* Add - Add SKU field to new product management experience [#34978](https://github.com/woocommerce/woocommerce/pull/34978) +* Add - Add the WooCommerce features engine [#34727](https://github.com/woocommerce/woocommerce/pull/34727) +* Add - Deprecate existing `wp wc cot migrate` command and replace with `wp wc cot sync`. [#34676](https://github.com/woocommerce/woocommerce/pull/34676) +* Add - Disable action buttons when product form is invalid [#34658](https://github.com/woocommerce/woocommerce/pull/34658) +* Add - Expand attributes list to display attributes list and allow removal and re-ordering. [#34841](https://github.com/woocommerce/woocommerce/pull/34841) +* Add - Images Product management [#34769](https://github.com/woocommerce/woocommerce/pull/34769) +* Add - Improve on feature incompatibility plugin screens. [#35063](https://github.com/woocommerce/woocommerce/pull/35063) +* Add - Render columns via action so that they can be hooked into. [#34900](https://github.com/woocommerce/woocommerce/pull/34900) +* Add - Support `wc_customer_bought_product` function in HPOS. [#34931](https://github.com/woocommerce/woocommerce/pull/34931) +* Add - The updates will mean that the github workflows use the playwright versions of the api-core-tests rather than the supertest versions of the tests. [#34935](https://github.com/woocommerce/woocommerce/pull/34935) +* Update - Don't show feature compatibility warnings for inactive plugins [#35333](https://github.com/woocommerce/woocommerce/pull/35333) +* Update - Update WooCommerce Blocks to 8.7.5 [#35428](https://github.com/woocommerce/woocommerce/pull/35428) +* Update - Improve the warnings about incompatibilities between plugins and features [#35198](https://github.com/woocommerce/woocommerce/pull/35198) +* Update - Additional payment methods on new WCPay promotion page (payment-welcome) [#34581](https://github.com/woocommerce/woocommerce/pull/34581) +* Update - Add Tiktok to free grow extensions list [#34953](https://github.com/woocommerce/woocommerce/pull/34953) +* Update - Allowing generic item type in new experimental SelectControl. [#34547](https://github.com/woocommerce/woocommerce/pull/34547) +* Update - Change order data store internal key to props for better representation. [#34627](https://github.com/woocommerce/woocommerce/pull/34627) +* Update - Changing inbox display to only 5 notes with the ability to load more. [#35003](https://github.com/woocommerce/woocommerce/pull/35003) +* Update - Deploy spotlight product tour treatment [#34859](https://github.com/woocommerce/woocommerce/pull/34859) +* Update - Track orders origin in WC_Tracker. [#35069](https://github.com/woocommerce/woocommerce/pull/35069) +* Update - Updates a few css selectors to be more robust [#34790](https://github.com/woocommerce/woocommerce/pull/34790) +* Update - Update WCPay promo requirements and ensure it's dismissed on every scenario [#35030](https://github.com/woocommerce/woocommerce/pull/35030) +* Dev - Add api-core-tests for playwright [#34835](https://github.com/woocommerce/woocommerce/pull/34835) +* Dev - Add fail-fast configuration to Playwright E2E tests. [#33977](https://github.com/woocommerce/woocommerce/pull/33977) +* Dev - Add new shippping class modal to a shipping class section in product page [#34937](https://github.com/woocommerce/woocommerce/pull/34937) +* Dev - Add shipping dimensions image to visualize the sizes of the product #34329 [#34857](https://github.com/woocommerce/woocommerce/pull/34857) +* Dev - Add tests for UI Revamp on Marketing Page. [#34840](https://github.com/woocommerce/woocommerce/pull/34840) +* Dev - Exclude "debug" module from babel compile to fix the tour kit stories loading error [#34831](https://github.com/woocommerce/woocommerce/pull/34831) +* Dev - Fix node and pnpm versions via engines [#34773](https://github.com/woocommerce/woocommerce/pull/34773) +* Dev - Improve the matching of plugins during the compatibility check. [#35070](https://github.com/woocommerce/woocommerce/pull/35070) +* Dev - Load size units to show it as a suffix of shipping dimensions fields #34329 [#34856](https://github.com/woocommerce/woocommerce/pull/34856) +* Dev - Match TypeScript version with syncpack [#34787](https://github.com/woocommerce/woocommerce/pull/34787) +* Dev - set the store country in the test step [#34972](https://github.com/woocommerce/woocommerce/pull/34972) +* Dev - Update Playwright to 1.26.0 and fix a few flaky tests [#34790](https://github.com/woocommerce/woocommerce/pull/34790) +* Dev - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35007](https://github.com/woocommerce/woocommerce/pull/35007) +* Tweak - Add hooks that fire before an HPOS order is deleted or trashed. [#34858](https://github.com/woocommerce/woocommerce/pull/34858) +* Tweak - Disable new-product-management-experience feature flag in development. [#34836](https://github.com/woocommerce/woocommerce/pull/34836) +* Tweak - Update copy in the payments welcome modal [#35031](https://github.com/woocommerce/woocommerce/pull/35031) +* Tweak - Update subdivision codes for New Zealand, to match current CLDR specification. [#35011](https://github.com/woocommerce/woocommerce/pull/35011) +* Tweak - When the primary order store is the posts table, and sync is enabled, propagate changes outside of dedicated migrations. [#34863](https://github.com/woocommerce/woocommerce/pull/34863) +* Performance - Support fetching order types in bulk to improve performance. [#34976](https://github.com/woocommerce/woocommerce/pull/34976) +* Enhancement - Add support for complex field queries for orders. [#34533](https://github.com/woocommerce/woocommerce/pull/34533) +* Enhancement - Also read from posts when reading from orders as a mittigation to direct write. [#34465](https://github.com/woocommerce/woocommerce/pull/34465) +* Enhancement - Enable async typeahead fields for the attribute and term fields within products. [#34744](https://github.com/woocommerce/woocommerce/pull/34744) +* Enhancement - Enchance tour experience for store location [#34697](https://github.com/woocommerce/woocommerce/pull/34697) + +**WooCommerce Blocks 8.7.0 & 8.7.1 & 8.7.2 & 8.7.3 & 8.7.4 & 8.7.5** + +* Enhancement - Improve visual consistency between block links. ([7340](https://github.com/woocommerce/woocommerce-blocks/pull/7340)) +* Enhancement - Update the titles of some inner blocks of the Cart block and remove the lock of the Cross-Sells parent block. ([7232](https://github.com/woocommerce/woocommerce-blocks/pull/7232)) +* Enhancement - Add filter for place order button label. ([7154](https://github.com/woocommerce/woocommerce-blocks/pull/7154)) +* Enhancement - Exposed data related to the checkout through wordpress/data stores. ([6612](https://github.com/woocommerce/woocommerce-blocks/pull/6612)) +* Enhancement - Add simple, large & two menus footer patterns. ([7306](https://github.com/woocommerce/woocommerce-blocks/pull/7306)) +* Enhancement - Add minimal, large, and essential header patterns. ([7292](https://github.com/woocommerce/woocommerce-blocks/pull/7292)) +* Enhancement - Add `showRemoveItemLink` as a new checkout filter to allow extensions to toggle the visibility of the `Remove item` button under each cart line item. ([7242](https://github.com/woocommerce/woocommerce-blocks/pull/7242)) +* Enhancement - Add support for a GT tracking ID for Google Analytics. ([7213](https://github.com/woocommerce/woocommerce-blocks/pull/7213)) +* Enhancement - Separate filter titles and filter controls by converting filter blocks to use Inner Blocks. ([6978](https://github.com/woocommerce/woocommerce-blocks/pull/6978)) +* Enhancement - StoreApi requests will return a `Cart-Token` header that can be used to retrieve the cart from the corresponding session via **GET** `/wc/store/v1/cart`. ([5953](https://github.com/woocommerce/woocommerce-blocks/pull/5953)) +* Fix - Fixed HTML rendering in description of active payment integrations. ([7313](https://github.com/woocommerce/woocommerce-blocks/pull/7313)) +* Fix - Hide the shipping address form from the Checkout when the "Force shipping to the customer billing address" is enabled. ([7268](https://github.com/woocommerce/woocommerce-blocks/pull/7268)) +* Fix - Fixed an error where adding new pages would cause an infinite loop and large amounts of memory use in redux. ([7256](https://github.com/woocommerce/woocommerce-blocks/pull/7256)) +* Fix - Ensure error messages containing HTML are shown correctly in the Cart and Checkout blocks. ([7231](https://github.com/woocommerce/woocommerce-blocks/pull/7231)) +* Fix - Prevent locked inner blocks from sometimes displaying twice. ([6676](https://github.com/woocommerce/woocommerce-blocks/pull/6676)) +* Fix - StoreApi `/checkout` endpoint now returns HTTP 402 instead of HTTP 400 when payment fails. ([7273](https://github.com/woocommerce/woocommerce-blocks/pull/7273)) +* Fix - Fix a problem that causes an infinite loop when inserting Cart block in wordpress.com. ([7367](https://github.com/woocommerce/woocommerce-blocks/pull/7367)) +* Fix - Fixed an issue where JavaScript errors would occur when more than one extension tried to filter specific payment methods in the Cart and Checkout blocks. ([7377](https://github.com/woocommerce/woocommerce-blocks/pull/7377)) +* Fix - Fixed a problem where Custom Order Tables compatibility declaration could fail due to the unpredictable plugin order load. ([7395](https://github.com/woocommerce/woocommerce-blocks/pull/7395)) +* Fix - Refactor useCheckoutAddress hook to enable "Use same address for billing" option in Editor ([7393](https://github.com/woocommerce/woocommerce-blocks/pull/7393)) +* Fix - Fixed an issue where the argument passed to `canMakePayment` contained the incorrect keys. Also fixed the current user's customer data appearing in the editor when editing the Checkout block. +* Fix - Compatibility fix for Cart and Checkout inner blocks for WordPress 6.1. + = 7.0.1 2022-11-01 = **WooCommerce** @@ -67,6 +180,7 @@ * Add - Support order searches as an integral part of how (COT) order queries work. [#34405](https://github.com/woocommerce/woocommerce/pull/34405) * Add - UI Revamp on Marketing Page with feature toggle. [#34642](https://github.com/woocommerce/woocommerce/pull/34642) * Add - Woo Mobile Welcome Page with Magic Link feature [#34637](https://github.com/woocommerce/woocommerce/pull/34637) +* Update - Update WooCommerce Blocks to 8.5.1 [#34807](https://github.com/woocommerce/woocommerce/pull/34807) * Update - Updates tracking parameters for marketing messages of mobile apps in New order mail. [#34717](https://github.com/woocommerce/woocommerce/pull/34717) * Update - Add an empty list of states for Saint Martin (French part) [#34521](https://github.com/woocommerce/woocommerce/pull/34521) * Update - Add Wish and Walmart to the platform options [#34541](https://github.com/woocommerce/woocommerce/pull/34541) @@ -187,7 +301,7 @@ * Fix - Minor changes to address Typescript errors after updating TS definitions [#34154](https://github.com/woocommerce/woocommerce/pull/34154) * Fix - Refactored homescreen component to use useQuery hook [#34183](https://github.com/woocommerce/woocommerce/pull/34183) * Fix - Support Cart/Checkout/My accounts/Terms settings in WC REST API [#34234](https://github.com/woocommerce/woocommerce/pull/34234) -* Fix - Use the default paymetn suggestions when woocommerce_show_marketplace_suggestions is set to no [#34083](https://github.com/woocommerce/woocommerce/pull/34083) +* Fix - Use the default payment suggestions when woocommerce_show_marketplace_suggestions is set to no [#34083](https://github.com/woocommerce/woocommerce/pull/34083) * Fix - Wrap default payment gateway strings in __() function call [#33987](https://github.com/woocommerce/woocommerce/pull/33987) * Add - Add default styles for block themes to ensure WooCommerce looks better out of the box with block themes that are not optimized for WooCommerce specifically. [#33518](https://github.com/woocommerce/woocommerce/pull/33518) * Add - Added tour for store location [#34137](https://github.com/woocommerce/woocommerce/pull/34137) @@ -260,8 +374,9 @@ * Enhancement - Reduce the amount of terms shown in attributes page [#33962](https://github.com/woocommerce/woocommerce/pull/33962) * Enhancement - Use method_exists instead of throwing in AbstractServiceProvider::reflect_class_or_callable [#33960](https://github.com/woocommerce/woocommerce/pull/33960) -**WooCommerce Blocks 8.1.0 & 8.2.0 & 8.2.1 & 8.3.0 & 8.3.1** +**WooCommerce Blocks 8.1.0 & 8.2.0 & 8.2.1 & 8.3.0 & 8.3.1 & 8.3.2** +* Enhancement - Add feedback box to the Cart & Checkout Inner Blocks in the inspector. ([6881](https://github.com/woocommerce/woocommerce-blocks/pull/6881)) * Enhancement - Enable the Cart and Checkout blocks when WooCommerce Blocks is bundled in WooCommerce Core.([6805](https://github.com/woocommerce/woocommerce-blocks/pull/6805)) * Enhancement - Refactor style-attributes hooks to add as global custom imports and remove relative import paths.([6870](https://github.com/woocommerce/woocommerce-blocks/pull/6870)) * Enhancement - Add the ability to register patterns by adding them under the "patterns" folder and add the new "WooCommerce Filters" pattern.([6861](https://github.com/woocommerce/woocommerce-blocks/pull/6861)) @@ -270,11 +385,8 @@ * Enhancement - Add filter URL support to filter blocks when filtering for All Products block.([6642](https://github.com/woocommerce/woocommerce-blocks/pull/6642)) * Enhancement - Add: Allow choosing between single and multiple sections.([6620](https://github.com/woocommerce/woocommerce-blocks/pull/6620)) * Enhancement - Cart endpoint for Store API (/wc/store/cart) now features cross-sell items based on cart contents.([6635](https://github.com/woocommerce/woocommerce-blocks/pull/6635)) -* Enhancement - Add feedback box to the Cart & Checkout Inner Blocks in the inspector. ([6881](https://github.com/woocommerce/woocommerce-blocks/pull/6881)) -* Enhancement - Refactor style-attributes hooks to add as global custom imports and remove relative import paths. ([6870](https://github.com/woocommerce/woocommerce-blocks/pull/6870)) -* Enhancement - Add notice to Cart and Checkout blocks' inspector controls which links to the list of compatible plugins. ([6869](https://github.com/woocommerce/woocommerce-blocks/pull/6869)) -* Enhancement - Add the ability to register patterns by adding them under the "patterns" folder and add the new "WooCommerce Filters" pattern. ([6861](https://github.com/woocommerce/woocommerce-blocks/pull/6861)) -* Enhancement - Enable the Cart and Checkout blocks when WooCommerce Blocks is bundled in WooCommerce Core. ([6805](https://github.com/woocommerce/woocommerce-blocks/pull/6805)) +* Fix - Fix Best Selling Products block ordering ([7025](https://github.com/woocommerce/woocommerce-blocks/pull/7025)) +* Fix - Prevent unnecessarily showing the item names in a shipping package if it's the only package. ([6899](https://github.com/woocommerce/woocommerce-blocks/pull/6899)) * Fix - Refactor Product Categories block to use block.json.([6875](https://github.com/woocommerce/woocommerce-blocks/pull/6875)) * Fix - Add font-weight controls to the Mini Cart block text.([6760](https://github.com/woocommerce/woocommerce-blocks/pull/6760)) * Fix - Fix proceed to checkout button not working for custom links.([6804](https://github.com/woocommerce/woocommerce-blocks/pull/6804)) @@ -286,10 +398,7 @@ * Fix - Fixes an issue where search lists would not preserve the case of the original item.([6551](https://github.com/woocommerce/woocommerce-blocks/pull/6551)) * Fix - Prevent Featured Product block from breaking when product is out of stock + hidden from catalog.([6640](https://github.com/woocommerce/woocommerce-blocks/pull/6640)) * Fix - Contrast improvement for checkout error messages when displayed over a theme's dark mode.([6292](https://github.com/woocommerce/woocommerce-blocks/pull/6292)) -* Fix - Refactor Product Categories block to use block.json. ([6875](https://github.com/woocommerce/woocommerce-blocks/pull/6875)) -* Fix - Fix: Update billing address when shipping address gets change in shipping calculator at Cart block. ([6823](https://github.com/woocommerce/woocommerce-blocks/pull/6823)) -* Fix - Fix: Add font-weight controls to the Mini Cart block text. ([6760](https://github.com/woocommerce/woocommerce-blocks/pull/6760)) -* Fix - Prevent unnecessarily showing the item names in a shipping package if it's the only package. ([6899](https://github.com/woocommerce/woocommerce-blocks/pull/6899)) + = 6.8.0 2022-08-09 = **WooCommerce** @@ -341,9 +450,9 @@ * Update - Implement bulk actions in the new orders admin list table. [#33687](https://github.com/woocommerce/woocommerce/pull/33687) * Update - Making default state of product image meta boxes more prominent. [#33707](https://github.com/woocommerce/woocommerce/pull/33707) * Update - Randomize the order of sections in Recommended Marketing Extensions [#33851](https://github.com/woocommerce/woocommerce/pull/33851) -* Update - Removed two-col task list expierments code [#33643](https://github.com/woocommerce/woocommerce/pull/33643) +* Update - Removed two-col task list experiments code [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Update - Remove legacy image sizes [#33772](https://github.com/woocommerce/woocommerce/pull/33772) -* Update - Set ddefault shipping methods when store country is the US and Jetpack is intalled [#33788](https://github.com/woocommerce/woocommerce/pull/33788) +* Update - Set default shipping methods when store country is the US and Jetpack is installed [#33788](https://github.com/woocommerce/woocommerce/pull/33788) * Update - Set smart shipping feature flags to true [#33819](https://github.com/woocommerce/woocommerce/pull/33819) * Update - Update display shipping task logic and add ReviewShippingOptions task [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Update - Update review shipping options task complete logic [#33650](https://github.com/woocommerce/woocommerce/pull/33650) @@ -352,7 +461,7 @@ * Update - Update wcpay suggestion UI in payment task [#33717](https://github.com/woocommerce/woocommerce/pull/33717) * Update - Update wcpay to include a mention of in-person payments for Canada [#33669](https://github.com/woocommerce/woocommerce/pull/33669) * Update - Update WooCommerce Blocks to 8.0.0 [#33736](https://github.com/woocommerce/woocommerce/pull/33736) -* Update - Uses WC_Data_Store directly to count the shipping zones to avoid any unncessary query to the D.B [#33643](https://github.com/woocommerce/woocommerce/pull/33643) +* Update - Uses WC_Data_Store directly to count the shipping zones to avoid any unnecessary query to the D.B [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Dev - Add playwright e2e README.md [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Dev - Add `$transaction_id` as arg to various `payment_complete` hooks. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) * Dev - Add `wc com extension install` CLI command [#33775](https://github.com/woocommerce/woocommerce/pull/33775) @@ -425,10 +534,10 @@ * Fix - Fix broken design of Single Product template in block themes when product had no reviews or additional info [#33329](https://github.com/woocommerce/woocommerce/pull/33329) * Fix - Clean up unused remote inbox notifications option [#33373](https://github.com/woocommerce/woocommerce/pull/33373) * Fix - Settings: fix Tracks event when enabling/disabling advanced features settings. [#33305](https://github.com/woocommerce/woocommerce/pull/33305) -* Fix - Fix accidently deleted method during merge for 33034. [#33142](https://github.com/woocommerce/woocommerce/pull/33142) +* Fix - Fix accidentally deleted method during merge for 33034. [#33142](https://github.com/woocommerce/woocommerce/pull/33142) * Fix - Fix the styling of the frame in the Leaderboard section of Analytics. [#33163](https://github.com/woocommerce/woocommerce/pull/33163) * Fix - Fix obw free extension rules for the marketing task with php 8 [#33329](https://github.com/woocommerce/woocommerce/pull/33329) -* Fix - Fix product tour splotlight location [#33329](https://github.com/woocommerce/woocommerce/pull/33329) +* Fix - Fix product tour spotlight location [#33329](https://github.com/woocommerce/woocommerce/pull/33329) * Fix - Fix product tour TypeError when reading innerHTML [#33448](https://github.com/woocommerce/woocommerce/pull/33448) * Fix - Fix: Content width issue and Classic Template blocks alignment issue for Twenty Twenty-Two. [#33329](https://github.com/woocommerce/woocommerce/pull/33329) * Fix - Fix typos in various cot migration messages. [#33329](https://github.com/woocommerce/woocommerce/pull/33329) @@ -548,7 +657,7 @@ * Dev - Remove the post_id column from the orders table, and adjust the SQL queries that count/get out of sync orders accordingly. [#32706](https://github.com/woocommerce/woocommerce/pull/32706) * Dev - Update woo admin ts config to have an isolated TS environment [#32616](https://github.com/woocommerce/woocommerce/pull/32616) * Dev - Updating scripts to use pnpm/Nx commands [#32943](https://github.com/woocommerce/woocommerce/pull/32943) -* Dev - Remove woo tracks type declaration from woo admin ./cleint. [#32937](https://github.com/woocommerce/woocommerce/pull/32937) +* Dev - Remove woo tracks type declaration from woo admin ./client. [#32937](https://github.com/woocommerce/woocommerce/pull/32937) * Dev - Fix react admin ./client type errors after updating @woocommerce/data types [#32735](https://github.com/woocommerce/woocommerce/pull/32735) * Dev - Removed temporary codepath added in #32603 since translation paths have been updated [#33226](https://github.com/woocommerce/woocommerce/pull/33226) * Enhancement - Add fallback image for payments task gateway icons [#32773](https://github.com/woocommerce/woocommerce/pull/32773) @@ -593,12 +702,12 @@ * Performance - Fix system status API requests that only query some fields [#32823](https://github.com/woocommerce/woocommerce/pull/32823) * Tweak - For Vietnam, the second street address line should be displayed but not required. [#32610](https://github.com/woocommerce/woocommerce/pull/32610) * Tweak - Comment: We're adding extra protections to a newly introduced feature; a further changelog entry is not needed. [#32771](https://github.com/woocommerce/woocommerce/pull/32771) -* Tweak - Fix spacing between the Paymetn logo assets in the payment banner experiment. [#33065](https://github.com/woocommerce/woocommerce/pull/33065) +* Tweak - Fix spacing between the payment logo assets in the payment banner experiment. [#33065](https://github.com/woocommerce/woocommerce/pull/33065) * Tweak - Comment: Omitting a changelog entry, because we're correcting an unreleased oversight. [#32744](https://github.com/woocommerce/woocommerce/pull/32744) * Tweak - Update TikTok onboarding icon [#32857](https://github.com/woocommerce/woocommerce/pull/32857) * Tweak - Fix typescript type errors in react admin ./client/shipping [#32688](https://github.com/woocommerce/woocommerce/pull/32688) * Tweak - Comment: Improves a newly added feature, so a further changelog entry is not required. [#32776](https://github.com/woocommerce/woocommerce/pull/32776) -* Tweak - Add wc-admin-deactivate-plugin to list of obselete notes so it gets deleted on upgrade [#32982](https://github.com/woocommerce/woocommerce/pull/32982) +* Tweak - Add wc-admin-deactivate-plugin to list of obsolete notes so it gets deleted on upgrade [#32982](https://github.com/woocommerce/woocommerce/pull/32982) * Tweak - Fix typescript type errors in react admin ./client/wp-admin-scripts [#32678](https://github.com/woocommerce/woocommerce/pull/32678) * Tweak - Move the file for the DatabaseUtil class to the proper directory according to its namespace. [#33109](https://github.com/woocommerce/woocommerce/pull/33109) * Tweak - Also allow getting category ID as option ID instead of term slug in wc-enhanced-select. [#32743](https://github.com/woocommerce/woocommerce/pull/32743) @@ -682,7 +791,7 @@ * Add - a new `woocommerce_generate_{$type}_html` action hook to generate custom field types in `WC_Settings_API` class objects. ([#31238](https://github.com/woocommerce/woocommerce/pull/31238)) * Add - Make the `$webhook` object available to consumers of the `woocommerce_webhook_options` action. ([#31292](https://github.com/woocommerce/woocommerce/pull/31292)) * Add - Pinterest extension to onboarding wizard and marketing task ([#32527](https://github.com/woocommerce/woocommerce/pull/32527)) -* Add - `order_item_display_meta` option to orders endpoint (REST API), to osupport filtering out variation meta. +* Add - `order_item_display_meta` option to orders endpoint (REST API), to support filtering out variation meta. * Add - new hooks to `order-tracking.php` form. ([#30320](https://github.com/woocommerce/woocommerce/pull/30320)) * Fix - Ensure that an existing order with auto-draft status won't be interpreted as pending when determining if the status has changed. ([#32571](https://github.com/woocommerce/woocommerce/pull/32571)) * Fix - bug in which tasks reminder bar was displayed on product screens. ([#32526](https://github.com/woocommerce/woocommerce/pull/32526)) @@ -699,7 +808,7 @@ * Fix - WCPay task add missing legal message within task. ([#32762](https://github.com/woocommerce/woocommerce/issues/32762)) * Tweak - Make it possible for downloadable files to be in an enabled or disabled state. * Tweak - UI changes for set up payments task -* Tweak - Update WCA deactivation hooks to work with WC deactvation. +* Tweak - Update WCA deactivation hooks to work with WC deactivation. * Tweak - Move feature flag config files to Woocommerce plugin to support unit test execution in the wp-env environment. * Tweak - Update progress header bar styles in task list ([#32498](https://github.com/woocommerce/woocommerce/pull/32498)) * Tweak - Update country strings, rename Swaziland to Eswatini (per CLDR R41 update). ([#31185](https://github.com/woocommerce/woocommerce/pull/31185)) @@ -712,7 +821,7 @@ * Dev - Merge WCA install routines to the core * Dev - Remove `load_plugin_textdomain` method from admin plugin. * Dev - Simplify the WooCommerce Admin init routine. ([#32489](https://github.com/woocommerce/woocommerce/pull/32489)) -* Dev - Generic migration support for migration from posts + postsmeta table to any custom table. Additionaly, implement migrations to various COT tables using this generic support. +* Dev - Generic migration support for migration from posts + postsmeta table to any custom table. Additionally, implement migrations to various COT tables using this generic support. * Dev - Remove Pinterest extension from OBW ([#32626](https://github.com/woocommerce/woocommerce/pull/32626)) * Dev - Revert back menu position to floats as string for WP compatibility. * Dev - Enable the "Save changes" button within the variations panel when a textfield receives input. ([#32589](https://github.com/woocommerce/woocommerce/pull/32589)) @@ -841,7 +950,7 @@ * Enhancement - Add support for the global style for the On-Sale Badge block. ([5565](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/*565)) * Enhancement - Add support for the global style for the Attribute Filter block. ([5557](https://github.com/woocommerce/woocommerce-gutenberg-products-block/*ull/5557)) * Enhancement - Category List block: Add support for global style. ([5516](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5516)) -* Fix - Fixed typo in `wooocommerce_store_api_validate_add_to_cart` and `wooocommerce_store_api_validate_cart_item` hook names. ([5926](https://github.com/*oocommerce/woocommerce-gutenberg-products-block/pull/5926)) +* Fix - Fixed typo in `woocommerce_store_api_validate_add_to_cart` and `woocommerce_store_api_validate_cart_item` hook names. ([5926](https://github.com/*oocommerce/woocommerce-gutenberg-products-block/pull/5926)) * Fix - Fix loading WC core translations in locales where WC Blocks is not localized for some strings. ([5910](https://github.com/woocommerce/*oocommerce-gutenberg-products-block/pull/5910)) * Fix - Fixed an issue where clear customizations functionality was not working for WooCommerce templates. ([5746](https://github.com/woocommerce/*oocommerce-gutenberg-products-block/pull/5746)) * Fix - Fixed hover and focus states for button components. ([5712](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5712)) @@ -1133,7 +1242,7 @@ * Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). ( [#761](https://github.com/woocommerce/action-scheduler/pull/761) ) * Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). ( [#768](https://github.com/woocommerce/action-scheduler/pull/768) ) * Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). ( [#762](https://github.com/woocommerce/action-scheduler/pull/762) ) -* Dev - Improve actions table indicies (props @glagonikas). ( [#774](https://github.com/woocommerce/action-scheduler/pull/774) & [#777](https://github.com/woocommerce/action-scheduler/pull/777) ) +* Dev - Improve actions table indices (props @glagonikas). ( [#774](https://github.com/woocommerce/action-scheduler/pull/774) & [#777](https://github.com/woocommerce/action-scheduler/pull/777) ) * Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). ( [#769](https://github.com/woocommerce/action-scheduler/pull/769) & [#778](https://github.com/woocommerce/action-scheduler/pull/778) ) * Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). ( [#763](https://github.com/woocommerce/action-scheduler/pull/763) & [#779](https://github.com/woocommerce/action-scheduler/pull/779) ) * Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). ( [#773](https://github.com/woocommerce/action-scheduler/pull/773) ) @@ -1254,7 +1363,7 @@ * Fix - Ensure homescreen defaults to single column layout. ( [#7969](https://github.com/woocommerce/woocommerce-admin/issues/7969) ) * Fix: Fix shipping task not offering step 3. ( [#7985](https://github.com/woocommerce/woocommerce-admin/issues/7985) ) * Add - Add Avalara to tax task ( [#7874](https://github.com/woocommerce/woocommerce-admin/issues/7874) ) -* Add - Add 2col expirement. ( [#7872](https://github.com/woocommerce/woocommerce-admin/issues/7872) ) +* Add - Add 2col experiment. ( [#7872](https://github.com/woocommerce/woocommerce-admin/issues/7872) ) * Add - Added two column experimental task list ( [#7669](https://github.com/woocommerce/woocommerce-admin/issues/7669) ) * Add - Add header cards for all tasks in Tasklist UI experiment ( [#7838](https://github.com/woocommerce/woocommerce-admin/issues/7838) ) * Add - Add onboarding task docs ( [#7762](https://github.com/woocommerce/woocommerce-admin/issues/7762) ) @@ -1676,7 +1785,7 @@ = 5.5.2 2021-07-22 = * Fix - Add a new option allowing product downloads to be served using redirects as a last resort. #30288 -* Fix - Remove unnecessary seacrh related 'where' clause added in the 'post_clauses' hook handling. #30335 +* Fix - Remove unnecessary search related 'where' clause added in the 'post_clauses' hook handling. #30335 * Fix - Check before calling $screen method to make sure its not null. #30277 **WooCommerce Admin - 2.4.4 & 2.4.3 & 2.4.2 ** @@ -1781,7 +1890,7 @@ * Fix - RemoteFreeExtension hide bundle when all of its plugins are not visible #7182 * Fix - Issue where summary stats were not showing in Analytics > Stock. #7161 * Fix - Rule Processing Transformer to handle dotNotation default value #7009 -* Fix - Remove Navigation's uneeded SlotFill context #6832 +* Fix - Remove Navigation's unneeded SlotFill context #6832 * Fix - Report filters expecting specific ordering. #6847 * Fix - Render bug with report comparison mode selections. #6862 * Fix - Throw exception if the data store cannot be loaded when trying to use notes. #6771 @@ -1937,7 +2046,7 @@ * Fix - Make pagination buttons height and width consistent #6725 * Fix - Retain persisted queries when navigating to Homescreen #6614 * Fix - Update folded header style #6724 -* Fix - Unreleated variations showing up in the Products reports #6647 +* Fix - Unrelated variations showing up in the Products reports #6647 * Fix - Check active plugins before getting the PayPal onboarding status #6625 * Fix - Remove no-reply from inbox notification emails #6644 * Fix - Set up shipping costs task, redirect to shipping settings after completion. #6791 @@ -2360,7 +2469,7 @@ * Fix - Onboarding - Fixed "Business Details" error. #6271 * Fix - Show management links when only main task list is hidden. #6291 * Fix - Correct the Klarna slug. #6440 -* Add - new inbox message - Getting started in Ecommerce - watch this webinar. #6086 +* Add - new inbox message - Getting started in e-commerce - watch this webinar. #6086 * Add - Remote inbox notifications contains comparison and fix product rule. #6073 * Add - Task list payments - include Mollie as an option. #6257 * Update - store deprecation welcome modal support doc link #6094 @@ -2387,7 +2496,7 @@ * Enhancements - Fix: Added fallback styling for screen reader text. ([3557](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3557)) * Fix - Ensure empty categories are correctly hidden in the product categories block. ([3765](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3765)) * Fix - Added missing wrapper div within FeaturedCategory and FeatureProduct blocks. ([3746](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3746)) -* Fix - Set correct text color in BlockErrorBoundry notices. ([3738](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3738)) +* Fix - Set correct text color in BlockErrorBoundary notices. ([3738](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3738)) * Fix - Hidden cart item meta data will not be rendered in the Cart and Checkout blocks. ([3732](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3732)) * Fix - Improved accessibility of product image links in the products block by using correct aria tags and hiding empty image placeholders. ([3722](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3722)) * Fix - Add missing aria-label for stars image in the review-list-item component. ([3706](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3706)) @@ -2805,7 +2914,7 @@ * 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 - Add a `NumberUtil::round` method to workaround a breaking change in the built-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. @@ -2918,7 +3027,7 @@ - Dev: Store Profiler - Industry step: reduced padding and removed industry #5157 - Dev: Remove product settings video note #5213 - Enhancement: Add free local shipping zone on profile complete #4857 -- Enhancement: Add woocommerce/tracks pacakage #5107 +- Enhancement: Add woocommerce/tracks package #5107 - Enhancement: Add filter to allow modification of report columns #4984 - Enhancement: Add WooCommerce Mobile Banner #5037 - Enhancement: Add Product Attribute advanced filter #5038 @@ -2992,7 +3101,7 @@ * Localization - Added postcode validation for Bosnia and Herzegovina. #27048 * Localization - Added the postcode validation for Liechtenstein. #27059 * Localization - Add i18n locale information for Liechtenstein, Switzerland and Austria. #27193 -* Tweak - Increase priority of `admin_body_class` filter to avoid comflict with plugins that incorrectly remove all body classes from WP. #27426 +* Tweak - Increase priority of `admin_body_class` filter to avoid conflict with plugins that incorrectly remove all body classes from WP. #27426 * Tweak - Rename built-in PayPal payment method to PayPal Standard. #27468 * Fix - Remove whitespace within a link. #26897 * Fix - `get_review_count_for_product` return all comments count not only 'review' types #26928 @@ -3103,7 +3212,7 @@ * Fix - After clicking to update WooCommerce, the user will stay in the same page instead of being redirected to the "Settings" page. #27172 * Fix - "Product type" dropdown missing from Product's data meta box on WP 5.5. #27170 * Fix - Removed the JETPACK_AUTOLOAD_DEV define. #27185 -* Fix - Fixed "virtual" and "downlodable" pointers on product walkthrough. #27145 +* Fix - Fixed "virtual" and "downloadable" pointers on product walkthrough. #27145 * Fix - Updated tested up to for WordPress 5.5. #27334 * Dev - Update WooCommerce Admin version to v1.4.0. #27378 * Dev - Upgraded to v2.2 of Jetpack Autoloader. #27358 @@ -3180,7 +3289,7 @@ * Performance - Don't load shortcode Cart and Checkout scripts when using the blocks. #2842 * Performance - Scripts only relevant to the frontend side of blocks are no longer loaded in the editor. #2788 * Performance - Lazy Loading Atomic Components. #2777 -* Pefactor - Remove dashicon classes. #2848 +* Refactor - Remove dashicon classes. #2848 **WooCommerce Blocks 3.1.0** * Fix - Missing permissions_callback arg in StoreApi route definitions. #2926 @@ -3270,7 +3379,7 @@ * Enhancement - Add API tool to verify base DB tables. woocommerce/woocommerce-rest-api#188 **WooCommerce Admin 1.3.0** -* Enhancement - Add Jetpack stats to performance indicatorts / homepage #4291 +* Enhancement - Add Jetpack stats to performance indicators / homepage #4291 * Enhancement - New "Store Management" quick links card on WooCommerce home screen. #4350 * Enhancement - Inbox notifications layout updates #4218 * Enhancement - New Home Screen #4303 @@ -3538,7 +3647,7 @@ * Dev - Made the default test source folders support the system tmp folder. #25923 * Dev - Add cart & checkout block/shortcode info to tracker data. #25932 * Dev - Make WC_Product_Data_Store_CPT::update_product_stock operations atomic. #26039 -* Dev - Adds usage data for the of cart & checkout blocks (currently in development in WooCommmerce Blocks plugin) to the WC Tracker snapshot. #26084 +* Dev - Adds usage data for the of cart & checkout blocks (currently in development in WooCommerce Blocks plugin) to the WC Tracker snapshot. #26084 * Dev - Implement some additional tracks for coupons, orders, and products. #26085 = 4.0.4 2022-03-10 = @@ -3716,7 +3825,7 @@ * Tweak - Cache checkout fragments and update DOM on change only. #24227 * Tweak - Eliminate extra update order AJAX request on checkout page load. #24271 * Tweak - Prevent billing address from being updated on shipping update. #24374 -* Tweak - Added a tooltip in the "Coupon expity date" field. #24749 +* Tweak - Added a tooltip in the "Coupon expiry date" field. #24749 * Tweak - Make phone numbers clickable in emails. #24786 * Tweak - Prevent PHP warnings in tracker if order doesn't have a created date yet. #24846 * Tweak - Capitalize "T" in "Move to Trash" phrase on order page in wp-admin to be consistent with product and coupon pages. #24867 @@ -3822,7 +3931,7 @@ * Tweak - Prevent filter per category while exporting product variations. #24517 * Tweak - Better wording for subtotal of items in cart and review order. #24440 * Tweak - Prevent new lines in product quantity in checkout details. #24311 -* Tweak - Add a tooltip in the "Coupon expity date" field. #24749 +* Tweak - Add a tooltip in the "Coupon expiry date" field. #24749 * Tweak - CSS styling changes for WP 5.3. #24832 * Template - Moved HTML for displaying product price filter widget to a new template `product price filter widget`. #23384 * Accessibility - Make $subtext color darker. #24739 From 91da0a15df43bb855abdde04d1b96cf1bdaa1e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alba=20Rinc=C3=B3n?= Date: Wed, 9 Nov 2022 09:59:08 +0100 Subject: [PATCH 149/149] Update WooCommerce blocks package to 8.9.0 (#35521) --- .../bin/composer/mozart/composer.lock | 50 +++++++++--------- .../bin/composer/phpcs/composer.lock | 24 +++++---- .../bin/composer/phpunit/composer.lock | 12 ++--- .../woocommerce/bin/composer/wp/composer.lock | 52 +++++++++---------- .../changelog/update-woocommerce-blocks-8.9.0 | 4 ++ plugins/woocommerce/composer.json | 2 +- plugins/woocommerce/composer.lock | 14 ++--- 7 files changed, 83 insertions(+), 75 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock index eea80f8bd52..1b31825ab7b 100644 --- a/plugins/woocommerce/bin/composer/mozart/composer.lock +++ b/plugins/woocommerce/bin/composer/mozart/composer.lock @@ -13,12 +13,12 @@ "source": { "type": "git", "url": "https://github.com/coenjacobs/mozart.git", - "reference": "75ae1f91f04bbbd4b6edff282a483dfe611b2cea" + "reference": "4f9d00fbc3b3e39f4e334434fe058e516ad82291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/75ae1f91f04bbbd4b6edff282a483dfe611b2cea", - "reference": "75ae1f91f04bbbd4b6edff282a483dfe611b2cea", + "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/4f9d00fbc3b3e39f4e334434fe058e516ad82291", + "reference": "4f9d00fbc3b3e39f4e334434fe058e516ad82291", "shasum": "" }, "require": { @@ -29,9 +29,11 @@ }, "require-dev": { "mheap/phpunit-github-actions-printer": "^1.4", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-deprecation-rules": "^1.0", "phpunit/phpunit": "^8.5", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.4" + "squizlabs/php_codesniffer": "^3.5" }, "default-branch": true, "bin": [ @@ -64,20 +66,20 @@ "type": "github" } ], - "time": "2021-08-03T18:56:55+00:00" + "time": "2022-10-22T08:08:20+00:00" }, { "name": "league/flysystem", - "version": "1.1.9", + "version": "1.1.10", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "094defdb4a7001845300334e7c1ee2335925ef99" + "reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/094defdb4a7001845300334e7c1ee2335925ef99", - "reference": "094defdb4a7001845300334e7c1ee2335925ef99", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3239285c825c152bcc315fe0e87d6b55f5972ed1", + "reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1", "shasum": "" }, "require": { @@ -150,7 +152,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/1.1.9" + "source": "https://github.com/thephpleague/flysystem/tree/1.1.10" }, "funding": [ { @@ -158,7 +160,7 @@ "type": "other" } ], - "time": "2021-12-09T09:40:50+00:00" + "time": "2022-10-04T09:16:37+00:00" }, { "name": "league/mime-type-detection", @@ -266,16 +268,16 @@ }, { "name": "symfony/console", - "version": "v5.4.12", + "version": "v5.4.15", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" + "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", - "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "url": "https://api.github.com/repos/symfony/console/zipball/ea59bb0edfaf9f28d18d8791410ee0355f317669", + "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669", "shasum": "" }, "require": { @@ -345,7 +347,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.12" + "source": "https://github.com/symfony/console/tree/v5.4.15" }, "funding": [ { @@ -361,7 +363,7 @@ "type": "tidelift" } ], - "time": "2022-08-17T13:18:05+00:00" + "time": "2022-10-26T21:41:52+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1070,16 +1072,16 @@ }, { "name": "symfony/string", - "version": "v5.4.12", + "version": "v5.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" + "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", - "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", + "url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", + "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", "shasum": "" }, "require": { @@ -1136,7 +1138,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.12" + "source": "https://github.com/symfony/string/tree/v5.4.15" }, "funding": [ { @@ -1152,7 +1154,7 @@ "type": "tidelift" } ], - "time": "2022-08-12T17:03:11+00:00" + "time": "2022-10-05T15:16:54+00:00" } ], "aliases": [], diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock index ef7a75941e4..0e1a6def19c 100644 --- a/plugins/woocommerce/bin/composer/phpcs/composer.lock +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -146,16 +146,16 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "ddabec839cc003651f2ce695c938686d1086cf43" + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/ddabec839cc003651f2ce695c938686d1086cf43", - "reference": "ddabec839cc003651f2ce695c938686d1086cf43", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", "shasum": "" }, "require": { @@ -192,26 +192,27 @@ "paragonie", "phpcs", "polyfill", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2021-02-15T10:24:51+00:00" + "time": "2022-10-25T01:46:02+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.3", + "version": "2.1.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "d55de55f88697b9cdb94bccf04f14eb3b11cf308" + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/d55de55f88697b9cdb94bccf04f14eb3b11cf308", - "reference": "d55de55f88697b9cdb94bccf04f14eb3b11cf308", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", "shasum": "" }, "require": { @@ -246,13 +247,14 @@ "compatibility", "phpcs", "standards", + "static analysis", "wordpress" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2021-12-30T16:37:40+00:00" + "time": "2022-10-24T09:00:36+00:00" }, { "name": "sirbrillig/phpcs-changed", diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock index 19d941bd66a..1983c0a3a0b 100644 --- a/plugins/woocommerce/bin/composer/phpunit/composer.lock +++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock @@ -1112,16 +1112,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.4", + "version": "3.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db" + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", - "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", "shasum": "" }, "require": { @@ -1177,7 +1177,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" }, "funding": [ { @@ -1185,7 +1185,7 @@ "type": "github" } ], - "time": "2021-11-11T13:51:24+00:00" + "time": "2022-09-14T06:00:17+00:00" }, { "name": "sebastian/global-state", diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock index 021717abf55..7ef28cb0bfd 100644 --- a/plugins/woocommerce/bin/composer/wp/composer.lock +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -148,16 +148,16 @@ }, { "name": "gettext/languages", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/php-gettext/Languages.git", - "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa" + "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Languages/zipball/ed56dd2c7f4024cc953ed180d25f02f2640e3ffa", - "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", + "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", "shasum": "" }, "require": { @@ -206,7 +206,7 @@ ], "support": { "issues": "https://github.com/php-gettext/Languages/issues", - "source": "https://github.com/php-gettext/Languages/tree/2.9.0" + "source": "https://github.com/php-gettext/Languages/tree/2.10.0" }, "funding": [ { @@ -218,20 +218,20 @@ "type": "github" } ], - "time": "2021-11-11T17:30:39+00:00" + "time": "2022-10-18T15:00:10+00:00" }, { "name": "mck89/peast", - "version": "v1.14.0", + "version": "v1.15.0", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "70a728d598017e237118652b2fa30fbaa9d4ef6d" + "reference": "733cd8f62dcb8239094688063a92766bbfcbf523" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/70a728d598017e237118652b2fa30fbaa9d4ef6d", - "reference": "70a728d598017e237118652b2fa30fbaa9d4ef6d", + "url": "https://api.github.com/repos/mck89/peast/zipball/733cd8f62dcb8239094688063a92766bbfcbf523", + "reference": "733cd8f62dcb8239094688063a92766bbfcbf523", "shasum": "" }, "require": { @@ -244,7 +244,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.14.0-dev" + "dev-master": "1.15.0-dev" } }, "autoload": { @@ -266,9 +266,9 @@ "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.14.0" + "source": "https://github.com/mck89/peast/tree/v1.15.0" }, - "time": "2022-05-01T15:09:54+00:00" + "time": "2022-09-13T15:56:53+00:00" }, { "name": "mustache/mustache", @@ -553,16 +553,16 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.11.15", + "version": "v0.11.16", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "b6edd35988892ea1451392eb7a26d9dbe98c836d" + "reference": "c32e51a5c9993ad40591bc426b21f5422a5ed293" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/b6edd35988892ea1451392eb7a26d9dbe98c836d", - "reference": "b6edd35988892ea1451392eb7a26d9dbe98c836d", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/c32e51a5c9993ad40591bc426b21f5422a5ed293", + "reference": "c32e51a5c9993ad40591bc426b21f5422a5ed293", "shasum": "" }, "require": { @@ -601,22 +601,22 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.15" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.16" }, - "time": "2022-08-15T10:15:55+00:00" + "time": "2022-11-03T15:19:26+00:00" }, { "name": "wp-cli/wp-cli", - "version": "v2.6.0", + "version": "v2.7.1", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "dee13c2baf6bf972484a63f8b8dab48f7220f095" + "reference": "1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/dee13c2baf6bf972484a63f8b8dab48f7220f095", - "reference": "dee13c2baf6bf972484a63f8b8dab48f7220f095", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76", + "reference": "1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76", "shasum": "" }, "require": { @@ -634,7 +634,7 @@ "wp-cli/entity-command": "^1.2 || ^2", "wp-cli/extension-command": "^1.1 || ^2", "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^3.1.3" + "wp-cli/wp-cli-tests": "^3.1.6" }, "suggest": { "ext-readline": "Include for a better --prompt implementation", @@ -647,7 +647,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev" + "dev-master": "2.8.x-dev" } }, "autoload": { @@ -674,7 +674,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2022-01-25T16:31:27+00:00" + "time": "2022-10-17T23:10:42+00:00" } ], "aliases": [], diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 new file mode 100644 index 00000000000..f49cff81ad5 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update WooCommerce Blocks to 8.9.0 diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index f2a9ca4ef70..48554489572 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.4.2", - "woocommerce/woocommerce-blocks": "8.7.5" + "woocommerce/woocommerce-blocks": "8.9.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 809b2e3c2e7..f9ee1390780 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -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": "08d58a387b373d546fb53025a230e768", + "content-hash": "4ba153cfcffe43c11a5c994e21a822bb", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v8.7.5", + "version": "v8.9.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "0436c8afb8c3c34dd38aed2b7a0868e771036031" + "reference": "9ed8e59f2f78a2bd0198750ed314590802d8f3d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/0436c8afb8c3c34dd38aed2b7a0868e771036031", - "reference": "0436c8afb8c3c34dd38aed2b7a0868e771036031", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/9ed8e59f2f78a2bd0198750ed314590802d8f3d5", + "reference": "9ed8e59f2f78a2bd0198750ed314590802d8f3d5", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.5" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.9.0" }, - "time": "2022-10-31T14:54:55+00:00" + "time": "2022-11-08T11:37:31+00:00" } ], "packages-dev": [