Merge WooCommerce Brands into core (#50165)

* Move WooCommerce Brands into core

* Fix linting errors in brand-thumbnails.php

* More lint fixes for brand-thumbnails.php

* Fix lint issues in brand-description.php

* Fix lint errors for brand-thumbnails-description

* Lint errors: Fix taxonomy-product_brand

* Lint errors: fix taxonomy-product_brand

* Linting: Try adding a space between ignore command and docblock

* Another try to remove the lowercase file name error

* Another try removing lint error for lowercase files

* Lint errors: Fix brands-a-z.php

* More lint fixes for brands-a-z.php

* More lint fixes for brands-a-z.php

* Lint fixes: brand-description.php

* Another try fixing lint errors for brand-description.php

* Another try fixing lint errors for brand-description.php

* More fixes for brand-description.php

* Fix lint errors for Packages.php

* More fixes for Packages.php

* Linting fixes for Brands.php

* Added docblocks for WC_Widget_Brand_Thumbnails variables

* Add script to fix coding standards for files changed in branch

* Run autofix script for linting

* Lint fixes for class-wc-widget-brand-thumbnails.php

* More lint fixes for: class-wc-widget-brand-thumbnails.php

* More lint fixes for class-wc-widget-brand-thumbnails.php

* Lint fixes for class-wc-widget-brand-nav.php

* lint fixes: ignore docblocks

* Another try to fix missing docblocks

* Another try to fix missing docblocks

* Another try fixing missing docblocks

* Better messages for fix-branch.sh

* Fix lint errors in class-wc-widget-brand-description.php

* Fix linting errors in REST API and functions classes

* Fix linting issues in class-wc-brands.php

* More lint fixes for wc-brands.php

* More lint fixes for wc-brands.php

* Fix lint errors for wc-brands-coupons.php

* Fix lint errors for class-wc-brands-block-templates.php

* Fix linting errors for class-wc-brands-block-template-utils-duplicated.php

* Fix lint errors in class-wc-admin-brands.php

* More fixes in class-wc-admin-brands.php

* More class-wc-admin-brands.php

* More lint fixes for: class-wc-admin-brands.php

* More lint fixes for class-wc-admin-brands.php

* Transfer unit test

* Transfer e2e test

* Added specific versions to templates

* Added changelog

* Another try for HTML templates version

* Fix lint errors in test files

* More lint fixes

* Fix lint warnings

* Added brands to list of expected REST API fields

* More lint warning fixes

* More lint warning fixes

* Updated unit tests to include brands

* Remove whitespace

* Added declare( strict_types = 1); to all PHP files

* Added declare( strict_types = 1) to test file as well

* Fix: There must be exactly one blank line after the file comment

* Temporarily remove Brands e2e tests

* Move Brands blockified templates

* Remove script to fix lint errors in current branch

* Try removing pull-package-deps

* Bring back deps

* Commit pnpm-lock.yaml

* Add debug statements

* More debug statements

* Make regular expression more specific

* Make matches more specific

* Search only for PHP templates

* Bring back whitespace

* Remove unnecessary change

* Update pnpm-lock.yaml

* Change the way Brands files are included

* Include all files

* Prevent Brands assets from being double-enqueued

* Move Brands scripts handling into core

* Revert changes in the template-changes.ts script

* Use strict in_array

* Add scaffolding for Brands test

* Add more scaffolding for Brands tests

* Enhance e2e test by adding steps for creating a Brand

* Move Brands test to Playwright folder

* Added manifest

* Fix lint errors in tests

* Move Brands coupons test into core's coupons tests

* Fix linting error in tests

* Move all Brands initialization within the /Internal/Brands class

* Rename `$merged_packages` to `$merged_plugins`

* Add force disable method back

* Move Brands logic outside core files

* Rename admin styles

* Remove brands logic from core's admin class

* Roll back all changes in admin assets class

* Fix linting errors

* Move REST API logic to Brands main class

* Introduce an option to control whether the Brands package is enabled.
Prevent autoloader from loading classes already loaded by individual Packages.
Fix an issue with Brands admin styles.

* Bring back pnpm-lock

* Add comment

* Split long line into two

* Review default values for remote variant assignment

* Rename global functions and add polyfills for deprecated functions

* Bump versions

* Fix some lint errors

* More lint fixes

* Set woocommerce_remote_variant_assignment for Brands to be enabled for unit tests

* Replace reserved word class with class_name

* Another try to include Brands files in tests

* Remove Brands from REST API tests

* Skip Brands tests while Brands is behind a feature flag

* Lint fixes

* Remove empty line

* Added feature flag.

* Fix widgets form

* Fix lint errors for brand description widget

* Fix lint errors for brand description widget

* Fix lint errors

* Bump version

* Updated tooltips for Brands coupon restrictions to match core's

* Fix lint errors

* More lint fixes

* Add REST API v3 for Brands

---------

Co-authored-by: Walther Lalk <83255+dakota@users.noreply.github.com>
This commit is contained in:
Jason Kytros 2024-09-17 18:13:46 +03:00 committed by GitHub
parent 514b39eea0
commit ea6e7295ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 4894 additions and 11 deletions

View File

@ -1229,7 +1229,7 @@
"menu_title": "Core critical flows",
"tags": "reference",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md",
"hash": "472f5a240abe907fec83a8a9f88af6699f2d994aa7ae87faa1716a087baa66db",
"hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md",
"id": "e561b46694dba223c38b87613ce4907e4e14333a"
},
@ -1804,5 +1804,5 @@
"categories": []
}
],
"hash": "3cad7f812ae15abd4936a536cf56db63b0f1b549e26eeb3427fe45989647d58c"
"hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3"
}

View File

@ -147,13 +147,14 @@ These flows will continually evolve as the platform evolves with flows updated,
### Merchant - Settings
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------------------- | ---------------------------------------- |
| --------- | --------- |----------------------------------------|------------------------------------------|
| Merchant | Settings | Update General Settings | merchant/settings-general.spec.js |
| Merchant | Settings | Add Tax Rates | merchant/settings-tax.spec.js |
| Merchant | Settings | Add Shipping Zones | merchant/create-shipping-zones.spec.js |
| Merchant | Settings | Add Shipping Classes | merchant/create-shipping-classes.spec.js |
| Merchant | Settings | Enable local pickup for checkout block | merchant/settings-shipping.spec.js |
| Merchant | Settings | Update payment settings | admin-tasks/payment.spec.js |
| Merchant | Settings | Handle Product Brands | merchant/create-product-brand.spec.js |
### Merchant - Coupons

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Introduced Product Brands.

View File

@ -0,0 +1,3 @@
table.wp-list-table .column-taxonomy-product_brand {
width: 10%;
}

View File

@ -0,0 +1,173 @@
/* Brand description on archives */
.tax-product_brand .brand-description {
overflow: hidden;
zoom: 1;
}
.tax-product_brand .brand-description img.brand-thumbnail {
width: 25%;
float: right;
}
.tax-product_brand .brand-description .text {
width: 72%;
float: left;
}
/* Brand description widget */
.widget_brand_description img {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
width: 100%;
max-width: none;
height: auto;
margin: 0 0 1em;
}
/* Brand thumbnails widget */
ul.brand-thumbnails {
margin-left: 0;
margin-bottom: 0;
clear: both;
list-style: none;
}
ul.brand-thumbnails:before {
clear: both;
content: "";
display: table;
}
ul.brand-thumbnails:after {
clear: both;
content: "";
display: table;
}
ul.brand-thumbnails li {
float: left;
margin: 0 3.8% 1em 0;
padding: 0;
position: relative;
width: 22.05%; /* 4 columns */
}
ul.brand-thumbnails.fluid-columns li {
width: auto;
}
ul.brand-thumbnails:not(.fluid-columns) li.first {
clear: both;
}
ul.brand-thumbnails:not(.fluid-columns) li.last {
margin-right: 0;
}
ul.brand-thumbnails.columns-1 li {
width: 100%;
margin-right: 0;
}
ul.brand-thumbnails.columns-2 li {
width: 48%;
}
ul.brand-thumbnails.columns-3 li {
width: 30.75%;
}
ul.brand-thumbnails.columns-5 li {
width: 16.95%;
}
ul.brand-thumbnails.columns-6 li {
width: 13.5%;
}
.brand-thumbnails li img {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
width: 100%;
max-width: none;
height: auto;
margin: 0;
}
@media screen and (max-width: 768px) {
ul.brand-thumbnails:not(.fluid-columns) li {
width: 48% !important;
}
ul.brand-thumbnails:not(.fluid-columns) li.first {
clear: none;
}
ul.brand-thumbnails:not(.fluid-columns) li.last {
margin-right: 3.8%
}
ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(odd) {
clear: both;
}
ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(even) {
margin-right: 0;
}
}
/* Brand thumbnails description */
.brand-thumbnails-description li {
text-align: center;
}
.brand-thumbnails-description li .term-thumbnail img {
display: inline;
}
.brand-thumbnails-description li .term-description {
margin-top: 1em;
text-align: left;
}
/* A-Z Shortcode */
#brands_a_z h3:target {
text-decoration: underline;
}
ul.brands_index {
list-style: none outside;
overflow: hidden;
zoom: 1;
}
ul.brands_index li {
float: left;
margin: 0 2px 2px 0;
}
ul.brands_index li a, ul.brands_index li span {
border: 1px solid #ccc;
padding: 6px;
line-height: 1em;
float: left;
text-decoration: none;
}
ul.brands_index li span {
border-color: #eee;
color: #ddd;
}
ul.brands_index li a:hover {
border-width: 2px;
padding: 5px;
text-decoration: none;
}
ul.brands_index li a.active {
border-width: 2px;
padding: 5px;
}
div#brands_a_z a.top {
border: 1px solid #ccc;
padding: 4px;
line-height: 1em;
float: right;
text-decoration: none;
font-size: 0.8em;
}

View File

@ -0,0 +1,94 @@
/* global wc_enhanced_select_params */
/* global wpApiSettings */
jQuery( function( $ ) {
function getEnhancedSelectFormatString() {
return {
'language': {
errorLoading: function() {
// Workaround for https://github.com/select2/select2/issues/4355 instead of i18n_ajax_error.
return wc_enhanced_select_params.i18n_searching;
},
inputTooLong: function( args ) {
var overChars = args.input.length - args.maximum;
if ( 1 === overChars ) {
return wc_enhanced_select_params.i18n_input_too_long_1;
}
return wc_enhanced_select_params.i18n_input_too_long_n.replace( '%qty%', overChars );
},
inputTooShort: function( args ) {
var remainingChars = args.minimum - args.input.length;
if ( 1 === remainingChars ) {
return wc_enhanced_select_params.i18n_input_too_short_1;
}
return wc_enhanced_select_params.i18n_input_too_short_n.replace( '%qty%', remainingChars );
},
loadingMore: function() {
return wc_enhanced_select_params.i18n_load_more;
},
maximumSelected: function( args ) {
if ( args.maximum === 1 ) {
return wc_enhanced_select_params.i18n_selection_too_long_1;
}
return wc_enhanced_select_params.i18n_selection_too_long_n.replace( '%qty%', args.maximum );
},
noResults: function() {
return wc_enhanced_select_params.i18n_no_matches;
},
searching: function() {
return wc_enhanced_select_params.i18n_searching;
}
}
};
}
try {
$( document.body )
.on( 'wc-enhanced-select-init', function() {
// Ajax category search boxes
$( ':input.wc-brands-search' ).filter( ':not(.enhanced)' ).each( function() {
var select2_args = $.extend( {
allowClear : $( this ).data( 'allow_clear' ) ? true : false,
placeholder : $( this ).data( 'placeholder' ),
minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : 3,
escapeMarkup : function( m ) {
return m;
},
ajax: {
url: wpApiSettings.root + 'wc/v3/products/brands',
dataType: 'json',
delay: 250,
headers: {
'X-WP-Nonce': wpApiSettings.nonce
},
data: function( params ) {
return {
hide_empty: 1,
search: params.term
};
},
processResults: function( data ) {
const results = data
.map( term => ({ id: term.slug, text: term.name + ' (' + term.count + ')' }) )
return {
results
};
},
cache: true
}
}, getEnhancedSelectFormatString() );
$( this ).selectWoo( select2_args ).addClass( 'enhanced' );
});
})
.trigger( 'wc-enhanced-select-init' );
} catch( err ) {
// If select2 failed (conflict?) log the error but don't stop other scripts breaking.
window.console.log( err );
}
});

View File

@ -0,0 +1,792 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName.
/**
* Brands Admin Page
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce\Admin
* @version 9.4.0
*/
declare( strict_types = 1);
use Automattic\Jetpack\Constants;
/**
* WC_Brands_Admin class.
*/
class WC_Brands_Admin {
/**
* Settings array.
*
* @var array
*/
public $settings_tabs;
/**
* Admin fields.
*
* @var array
*/
public $fields = array();
/**
* __construct function.
*/
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'styles' ) );
add_action( 'product_brand_add_form_fields', array( $this, 'add_thumbnail_field' ) );
add_action( 'product_brand_edit_form_fields', array( $this, 'edit_thumbnail_field' ), 10, 1 );
add_action( 'created_term', array( $this, 'thumbnail_field_save' ), 10, 1 );
add_action( 'edit_term', array( $this, 'thumbnail_field_save' ), 10, 1 );
add_action( 'product_brand_pre_add_form', array( $this, 'taxonomy_description' ) );
add_filter( 'woocommerce_sortable_taxonomies', array( $this, 'sort_brands' ) );
add_filter( 'manage_edit-product_brand_columns', array( $this, 'columns' ) );
add_filter( 'manage_product_brand_custom_column', array( $this, 'column' ), 10, 3 );
add_filter( 'manage_product_posts_columns', array( $this, 'product_columns' ), 20, 1 );
add_filter(
'woocommerce_products_admin_list_table_filters',
function ( $args ) {
$args['product_brand'] = array( $this, 'render_product_brand_filter' );
return $args;
}
);
$this->settings_tabs = array(
'brands' => __( 'Brands', 'woocommerce' ),
);
// Hiding setting for future depreciation. Only users who have touched this settings should see it.
$setting_value = get_option( 'wc_brands_show_description' );
if ( is_string( $setting_value ) ) {
// Add the settings fields to each tab.
$this->init_form_fields();
add_action( 'woocommerce_get_sections_products', array( $this, 'add_settings_tab' ) );
add_action( 'woocommerce_get_settings_products', array( $this, 'add_settings_section' ), null, 2 );
}
add_action( 'woocommerce_update_options_catalog', array( $this, 'save_admin_settings' ) );
/* 2.1 */
add_action( 'woocommerce_update_options_products', array( $this, 'save_admin_settings' ) );
// Add brands filtering to the coupon creation screens.
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'add_coupon_brands_fields' ) );
add_action( 'woocommerce_coupon_options_save', array( $this, 'save_coupon_brands' ) );
// Permalinks.
add_filter( 'pre_update_option_woocommerce_permalinks', array( $this, 'validate_product_base' ) );
add_action( 'current_screen', array( $this, 'add_brand_base_setting' ) );
// CSV Import/Export Support.
// https://github.com/woocommerce/woocommerce/wiki/Product-CSV-Importer-&-Exporter
// Import.
add_filter( 'woocommerce_csv_product_import_mapping_options', array( $this, 'add_column_to_importer_exporter' ), 10 );
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', array( $this, 'add_default_column_mapping' ), 10 );
add_filter( 'woocommerce_product_import_inserted_product_object', array( $this, 'process_import' ), 10, 2 );
// Export.
add_filter( 'woocommerce_product_export_column_names', array( $this, 'add_column_to_importer_exporter' ), 10 );
add_filter( 'woocommerce_product_export_product_default_columns', array( $this, 'add_column_to_importer_exporter' ), 10 );
add_filter( 'woocommerce_product_export_product_column_brand_ids', array( $this, 'get_column_value_brand_ids' ), 10, 2 );
}
/**
* Add the settings for the new "Brands" subtab.
*
* @since 9.4.0
*
* @param array $settings Settings.
* @param array $current_section Current section.
*/
public function add_settings_section( $settings, $current_section ) {
if ( 'brands' === $current_section ) {
$settings = $this->settings;
}
return $settings;
}
/**
* Add a new "Brands" subtab to the "Products" tab.
*
* @since 9.4.0
* @param array $sections Sections.
*/
public function add_settings_tab( $sections ) {
$sections = array_merge( $sections, $this->settings_tabs );
return $sections;
}
/**
* Display coupon filter fields relating to brands.
*
* @since 9.4.0
* @return void
*/
public function add_coupon_brands_fields() {
global $post;
// Brands.
?>
<p class="form-field"><label for="product_brands"><?php esc_html_e( 'Product brands', 'woocommerce' ); ?></label>
<select id="product_brands" name="product_brands[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'Any brand', 'woocommerce' ); ?>">
<?php
$category_ids = (array) get_post_meta( $post->ID, 'product_brands', true );
$categories = get_terms(
array(
'taxonomy' => 'product_brand',
'orderby' => 'name',
'hide_empty' => false,
)
);
if ( $categories ) {
foreach ( $categories as $cat ) {
echo '<option value="' . esc_attr( $cat->term_id ) . '"' . selected( in_array( $cat->term_id, $category_ids, true ), true, false ) . '>' . esc_html( $cat->name ) . '</option>';
}
}
?>
</select>
<?php
echo wc_help_tip( esc_html__( 'A product must be associated with this brand for the coupon to remain valid or, for "Product Discounts", products with these brands will be discounted.', 'woocommerce' ) );
// Exclude Brands.
?>
<p class="form-field"><label for="exclude_product_brands"><?php esc_html_e( 'Exclude brands', 'woocommerce' ); ?></label>
<select id="exclude_product_brands" name="exclude_product_brands[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No brands', 'woocommerce' ); ?>">
<?php
$category_ids = (array) get_post_meta( $post->ID, 'exclude_product_brands', true );
$categories = get_terms(
array(
'taxonomy' => 'product_brand',
'orderby' => 'name',
'hide_empty' => false,
)
);
if ( $categories ) {
foreach ( $categories as $cat ) {
echo '<option value="' . esc_attr( $cat->term_id ) . '"' . selected( in_array( $cat->term_id, $category_ids, true ), true, false ) . '>' . esc_html( $cat->name ) . '</option>';
}
}
?>
</select>
<?php
echo wc_help_tip( esc_html__( 'Product must not be associated with these brands for the coupon to remain valid or, for "Product Discounts", products associated with these brands will not be discounted.', 'woocommerce' ) );
}
/**
* Save coupon filter fields relating to brands.
*
* @since 9.4.0
* @param int $post_id Post ID.
* @return void
*/
public function save_coupon_brands( $post_id ) {
$product_brands = isset( $_POST['product_brands'] ) ? array_map( 'intval', $_POST['product_brands'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$exclude_product_brands = isset( $_POST['exclude_product_brands'] ) ? array_map( 'intval', $_POST['exclude_product_brands'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing
// Save.
update_post_meta( $post_id, 'product_brands', $product_brands );
update_post_meta( $post_id, 'exclude_product_brands', $exclude_product_brands );
}
/**
* Prepare form fields to be used in the various tabs.
*/
public function init_form_fields() {
/**
* Filter Brands settings.
*
* @since 9.4.0
*
* @param array $settings Brands settings.
*/
$this->settings = apply_filters(
'woocommerce_brands_settings_fields',
array(
array(
'name' => __( 'Brands Archives', 'woocommerce' ),
'type' => 'title',
'desc' => '',
'id' => 'brands_archives',
),
array(
'name' => __( 'Show description', 'woocommerce' ),
'desc' => __( 'Choose to show the brand description on the archive page. Turn this off if you intend to use the description widget instead. Please note: this is only for themes that do not show the description.', 'woocommerce' ),
'tip' => '',
'id' => 'wc_brands_show_description',
'css' => '',
'std' => 'yes',
'type' => 'checkbox',
),
array(
'type' => 'sectionend',
'id' => 'brands_archives',
),
)
);
}
/**
* Enqueue scripts.
*
* @return void
*/
public function scripts() {
$screen = get_current_screen();
$version = Constants::get_constant( 'WC_VERSION' );
$suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
if ( 'edit-product' === $screen->id ) {
wp_register_script(
'wc-brands-enhanced-select',
WC()->plugin_url() . '/assets/js/admin/wc-brands-enhanced-select' . $suffix . '.js',
array( 'jquery', 'selectWoo', 'wc-enhanced-select', 'wp-api' ),
$version,
true
);
wp_localize_script(
'wc-brands-enhanced-select',
'wc_brands_enhanced_select_params',
array( 'ajax_url' => get_rest_url() . 'brands/search' )
);
wp_enqueue_script( 'wc-brands-enhanced-select' );
}
if ( in_array( $screen->id, array( 'edit-product_brand' ), true ) ) {
wp_enqueue_media();
wp_enqueue_style( 'woocommerce_admin_styles' );
}
}
/**
* Enqueue styles.
*
* @return void
*/
public function styles() {
$version = Constants::get_constant( 'WC_VERSION' );
wp_enqueue_style( 'brands-admin-styles', WC()->plugin_url() . '/assets/css/brands-admin.css', array(), $version );
}
/**
* Admin settings function.
*/
public function admin_settings() {
woocommerce_admin_fields( $this->settings );
}
/**
* Save admin settings function.
*/
public function save_admin_settings() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['section'] ) && 'brands' === $_GET['section'] ) {
woocommerce_update_options( $this->settings );
}
}
/**
* Category thumbnails.
*/
public function add_thumbnail_field() {
global $woocommerce;
?>
<div class="form-field">
<label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label>
<div id="product_cat_thumbnail" style="float:left;margin-right:10px;"><img src="<?php echo esc_url( wc_placeholder_img_src() ); ?>" width="60px" height="60px" /></div>
<div style="line-height:60px;">
<input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" />
<button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button>
<button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button>
</div>
<script type="text/javascript">
jQuery(function(){
// Only show the "remove image" button when needed
if ( ! jQuery('#product_cat_thumbnail_id').val() ) {
jQuery('.remove_image_button').hide();
}
// Uploading files
var file_frame;
jQuery(document).on( 'click', '.upload_image_button', function( event ){
event.preventDefault();
// If the media frame already exists, reopen it.
if ( file_frame ) {
file_frame.open();
return;
}
// Create the media frame.
file_frame = wp.media.frames.downloadable_file = wp.media({
title: '<?php echo esc_js( __( 'Choose an image', 'woocommerce' ) ); ?>',
button: {
text: '<?php echo esc_js( __( 'Use image', 'woocommerce' ) ); ?>',
},
multiple: false
});
// When an image is selected, run a callback.
file_frame.on( 'select', function() {
attachment = file_frame.state().get('selection').first().toJSON();
jQuery('#product_cat_thumbnail_id').val( attachment.id );
jQuery('#product_cat_thumbnail img').attr('src', attachment.url );
jQuery('.remove_image_button').show();
});
// Finally, open the modal.
file_frame.open();
});
jQuery(document).on( 'click', '.remove_image_button', function( event ){
jQuery('#product_cat_thumbnail img').attr('src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>');
jQuery('#product_cat_thumbnail_id').val('');
jQuery('.remove_image_button').hide();
return false;
});
});
</script>
<div class="clear"></div>
</div>
<?php
}
/**
* Edit thumbnail field row.
*
* @param WP_Term $term Current taxonomy term object.
*/
public function edit_thumbnail_field( $term ) {
global $woocommerce;
$image = '';
$thumbnail_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
if ( $thumbnail_id ) {
$image = wp_get_attachment_url( $thumbnail_id );
}
if ( empty( $image ) ) {
$image = wc_placeholder_img_src();
}
?>
<tr class="form-field">
<th scope="row" valign="top"><label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label></th>
<td>
<div id="product_cat_thumbnail" style="float:left;margin-right:10px;"><img src="<?php echo esc_url( $image ); ?>" width="60px" height="60px" /></div>
<div style="line-height:60px;">
<input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" value="<?php echo esc_attr( $thumbnail_id ); ?>" />
<button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button>
<button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button>
</div>
<script type="text/javascript">
jQuery(function(){
// Only show the "remove image" button when needed
if ( ! jQuery('#product_cat_thumbnail_id').val() )
jQuery('.remove_image_button').hide();
// Uploading files
var file_frame;
jQuery(document).on( 'click', '.upload_image_button', function( event ){
event.preventDefault();
// If the media frame already exists, reopen it.
if ( file_frame ) {
file_frame.open();
return;
}
// Create the media frame.
file_frame = wp.media.frames.downloadable_file = wp.media({
title: '<?php echo esc_js( __( 'Choose an image', 'woocommerce' ) ); ?>',
button: {
text: '<?php echo esc_js( __( 'Use image', 'woocommerce' ) ); ?>',
},
multiple: false
});
// When an image is selected, run a callback.
file_frame.on( 'select', function() {
attachment = file_frame.state().get('selection').first().toJSON();
jQuery('#product_cat_thumbnail_id').val( attachment.id );
jQuery('#product_cat_thumbnail img').attr('src', attachment.url );
jQuery('.remove_image_button').show();
});
// Finally, open the modal.
file_frame.open();
});
jQuery(document).on( 'click', '.remove_image_button', function( event ){
jQuery('#product_cat_thumbnail img').attr('src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>');
jQuery('#product_cat_thumbnail_id').val('');
jQuery('.remove_image_button').hide();
return false;
});
});
</script>
<div class="clear"></div>
</td>
</tr>
<?php
}
/**
* Saves thumbnail field.
*
* @param int $term_id Term ID.
*
* @return void
*/
public function thumbnail_field_save( $term_id ) {
if ( isset( $_POST['product_cat_thumbnail_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_term_meta( $term_id, 'thumbnail_id', absint( $_POST['product_cat_thumbnail_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
}
/**
* Description for brand page.
*/
public function taxonomy_description() {
echo wp_kses_post( wpautop( __( 'Brands be added and managed from this screen. You can optionally upload a brand image to display in brand widgets and on brand archives', 'woocommerce' ) ) );
}
/**
* Sort brands function.
*
* @param array $sortable Sortable array.
*/
public function sort_brands( $sortable ) {
$sortable[] = 'product_brand';
return $sortable;
}
/**
* Add brands column in second-to-last position.
*
* @since 9.4.0
* @param mixed $columns Columns.
* @return array
*/
public function product_columns( $columns ) {
if ( empty( $columns ) ) {
return $columns;
}
$column_index = 'taxonomy-product_brand';
$brands_column = $columns[ $column_index ];
unset( $columns[ $column_index ] );
return array_merge(
array_slice( $columns, 0, -2, true ),
array( $column_index => $brands_column ),
array_slice( $columns, -2, null, true )
);
}
/**
* Columns function.
*
* @param mixed $columns Columns.
*/
public function columns( $columns ) {
if ( empty( $columns ) ) {
return $columns;
}
$new_columns = array();
$new_columns['cb'] = $columns['cb'];
$new_columns['thumb'] = __( 'Image', 'woocommerce' );
unset( $columns['cb'] );
$columns = array_merge( $new_columns, $columns );
return $columns;
}
/**
* Column function.
*
* @param mixed $columns Columns.
* @param mixed $column Column.
* @param mixed $id ID.
*/
public function column( $columns, $column, $id ) {
if ( 'thumb' === $column ) {
global $woocommerce;
$image = '';
$thumbnail_id = get_term_meta( $id, 'thumbnail_id', true );
if ( $thumbnail_id ) {
$image = wp_get_attachment_url( $thumbnail_id );
}
if ( empty( $image ) ) {
$image = wc_placeholder_img_src();
}
$columns .= '<img src="' . $image . '" alt="Thumbnail" class="wp-post-image" height="48" width="48" />';
}
return $columns;
}
/**
* Renders either dropdown or a search field for brands depending on the threshold value of
* woocommerce_product_brand_filter_threshold filter.
*/
public function render_product_brand_filter() {
// phpcs:disable WordPress.Security.NonceVerification
$brands_count = (int) wp_count_terms( 'product_brand' );
$current_brand_slug = wc_clean( wp_unslash( $_GET['product_brand'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
/**
* Filter the brands threshold count.
*
* @since 9.4.0
*
* @param int $value Threshold.
*/
if ( $brands_count <= apply_filters( 'woocommerce_product_brand_filter_threshold', 100 ) ) {
wc_product_dropdown_categories(
array(
'pad_counts' => true,
'show_count' => true,
'orderby' => 'name',
'selected' => $current_brand_slug,
'show_option_none' => __( 'Filter by brand', 'woocommerce' ),
'option_none_value' => '',
'value_field' => 'slug',
'taxonomy' => 'product_brand',
'name' => 'product_brand',
'class' => 'dropdown_product_brand',
)
);
} else {
$current_brand = $current_brand_slug ? get_term_by( 'slug', $current_brand_slug, 'product_brand' ) : '';
$selected_option = '';
if ( $current_brand_slug && $current_brand ) {
$selected_option = '<option value="' . esc_attr( $current_brand_slug ) . '" selected="selected">' . esc_html( htmlspecialchars( wp_kses_post( $current_brand->name ) ) ) . '</option>';
}
$placeholder = esc_attr__( 'Filter by brand', 'woocommerce' );
?>
<select class="wc-brands-search" name="product_brand" data-placeholder="<?php echo $placeholder; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" data-allow_clear="true">
<?php echo $selected_option; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</select>
<?php
}
// phpcs:enable WordPress.Security.NonceVerification
}
/**
* Add brand base permalink setting.
*/
public function add_brand_base_setting() {
$screen = get_current_screen();
if ( ! $screen || 'options-permalink' !== $screen->id ) {
return;
}
add_settings_field(
'woocommerce_product_brand_slug',
__( 'Product brand base', 'woocommerce' ),
array( $this, 'product_brand_slug_input' ),
'permalink',
'optional'
);
$this->save_permalink_settings();
}
/**
* Add a slug input box.
*/
public function product_brand_slug_input() {
$permalink = get_option( 'woocommerce_brand_permalink', '' );
?>
<input name="woocommerce_product_brand_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $permalink ); ?>" placeholder="<?php echo esc_attr_x( 'brand', 'slug', 'woocommerce' ); ?>" />
<?php
}
/**
* Save permalnks settings.
*
* We need to save the options ourselves;
* settings api does not trigger save for the permalinks page.
*/
public function save_permalink_settings() {
if ( ! is_admin() ) {
return;
}
if ( isset( $_POST['permalink_structure'], $_POST['wc-permalinks-nonce'], $_POST['woocommerce_product_brand_slug'] ) && wp_verify_nonce( wp_unslash( $_POST['wc-permalinks-nonce'] ), 'wc-permalinks' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
update_option( 'woocommerce_brand_permalink', wc_sanitize_permalink( trim( wc_clean( wp_unslash( $_POST['woocommerce_product_brand_slug'] ) ) ) ) );
}
}
/**
* Validate the product base.
*
* Must have an additional slug, not just the brand as the base.
*
* @param array $value Value.
*/
public function validate_product_base( $value ) {
if ( '/%product_brand%/' === trailingslashit( $value['product_base'] ) ) {
$value['product_base'] = '/' . _x( 'product', 'slug', 'woocommerce' ) . $value['product_base'];
}
return $value;
}
/**
* Add csv column for importing/exporting.
*
* @param array $options Mapping options.
* @return array $options
*/
public function add_column_to_importer_exporter( $options ) {
$options['brand_ids'] = __( 'Brands', 'woocommerce' );
return $options;
}
/**
* Add default column mapping.
*
* @param array $mappings Mappings.
* @return array $mappings
*/
public function add_default_column_mapping( $mappings ) {
$new_mapping = array( __( 'Brands', 'woocommerce' ) => 'brand_ids' );
return array_merge( $mappings, $new_mapping );
}
/**
* Add brands to newly imported product.
*
* @param WC_Product $product Product being imported.
* @param array $data Raw CSV data.
*/
public function process_import( $product, $data ) {
if ( empty( $data['brand_ids'] ) ) {
return;
}
$brand_ids = array_map( 'intval', $this->parse_brands_field( $data['brand_ids'] ) );
wp_set_object_terms( $product->get_id(), $brand_ids, 'product_brand' );
}
/**
* Parse brands field from a CSV during import.
*
* Based on WC_Product_CSV_Importer::parse_categories_field()
*
* @param string $value Field value.
* @return array
*/
public function parse_brands_field( $value ) {
// Based on WC_Product_Importer::explode_values().
$values = str_replace( '\\,', '::separator::', explode( ',', $value ) );
$row_terms = array();
foreach ( $values as $row_value ) {
$row_terms[] = trim( str_replace( '::separator::', ',', $row_value ) );
}
$brands = array();
foreach ( $row_terms as $row_term ) {
$parent = null;
// WC Core uses '>', but for some reason it's already escaped at this point.
$_terms = array_map( 'trim', explode( '&gt;', $row_term ) );
$total = count( $_terms );
foreach ( $_terms as $index => $_term ) {
$term = term_exists( $_term, 'product_brand', $parent );
if ( is_array( $term ) ) {
$term_id = $term['term_id'];
} else {
$term = wp_insert_term( $_term, 'product_brand', array( 'parent' => intval( $parent ) ) );
if ( is_wp_error( $term ) ) {
break; // We cannot continue if the term cannot be inserted.
}
$term_id = $term['term_id'];
}
// Only requires assign the last category.
if ( ( 1 + $index ) === $total ) {
$brands[] = $term_id;
} else {
// Store parent to be able to insert or query brands based in parent ID.
$parent = $term_id;
}
}
}
return $brands;
}
/**
* Get brands column value for csv export.
*
* @param string $value What will be exported.
* @param WC_Product $product Product being exported.
* @return string Brands separated by commas and child brands as "parent > child".
*/
public function get_column_value_brand_ids( $value, $product ) {
$brand_ids = wp_parse_id_list( wp_get_post_terms( $product->get_id(), 'product_brand', array( 'fields' => 'ids' ) ) );
if ( ! count( $brand_ids ) ) {
return '';
}
// Based on WC_CSV_Exporter::format_term_ids().
$formatted_brands = array();
foreach ( $brand_ids as $brand_id ) {
$formatted_term = array();
$ancestor_ids = array_reverse( get_ancestors( $brand_id, 'product_brand' ) );
foreach ( $ancestor_ids as $ancestor_id ) {
$term = get_term( $ancestor_id, 'product_brand' );
if ( $term && ! is_wp_error( $term ) ) {
$formatted_term[] = $term->name;
}
}
$term = get_term( $brand_id, 'product_brand' );
if ( $term && ! is_wp_error( $term ) ) {
$formatted_term[] = $term->name;
}
$formatted_brands[] = implode( ' > ', $formatted_term );
}
// Based on WC_CSV_Exporter::implode_values().
$values_to_implode = array();
foreach ( $formatted_brands as $brand ) {
$brand = (string) is_scalar( $brand ) ? $brand : '';
$values_to_implode[] = str_replace( ',', '\\,', $brand );
}
return implode( ', ', $values_to_implode );
}
}
$GLOBALS['WC_Brands_Admin'] = new WC_Brands_Admin();

View File

@ -0,0 +1,369 @@
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
declare( strict_types = 1);
use Automattic\WooCommerce\Blocks\Options;
//phpcs:disable Squiz.Classes.ClassFileName.NoMatch
/**
* BlockTemplateUtils class used for serving block templates from Woo Blocks.
* IMPORTANT: These methods have been duplicated from Gutenberg/lib/full-site-editing/block-templates.php as those functions are not for public usage.
*
* For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @version 9.4.0
*/
class BlockTemplateUtilsDuplicated {
/**
* Directory names for block templates
*
* Directory names conventions for block templates have changed with Gutenberg 12.1.0,
* however, for backwards-compatibility, we also keep the older conventions, prefixed
* with `DEPRECATED_`.
*
* @var array {
* @var string DEPRECATED_TEMPLATES Old directory name of the block templates directory.
* @var string DEPRECATED_TEMPLATE_PARTS Old directory name of the block template parts directory.
* @var string TEMPLATES_DIR_NAME Directory name of the block templates directory.
* @var string TEMPLATE_PARTS_DIR_NAME Directory name of the block template parts directory.
* }
*/
protected const DIRECTORY_NAMES = array(
'DEPRECATED_TEMPLATES' => 'block-templates',
'DEPRECATED_TEMPLATE_PARTS' => 'block-template-parts',
'TEMPLATES' => 'templates',
'TEMPLATE_PARTS' => 'parts',
);
/**
* WooCommerce plugin slug
*
* This is used to save templates to the DB which are stored against this value in the wp_terms table.
*
* @var string
*/
protected const PLUGIN_SLUG = 'woocommerce/woocommerce';
/**
* Returns an array containing the references of
* the passed blocks and their inner blocks.
*
* @param array $blocks array of blocks.
*
* @return array block references to the passed blocks and their inner blocks.
*/
public static function gutenberg_flatten_blocks( &$blocks ) {
$all_blocks = array();
$queue = array();
foreach ( $blocks as &$block ) {
$queue[] = &$block;
}
$queue_count = count( $queue );
while ( $queue_count > 0 ) {
$block = &$queue[0];
array_shift( $queue );
$all_blocks[] = &$block;
if ( ! empty( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as &$inner_block ) {
$queue[] = &$inner_block;
}
}
$queue_count = count( $queue );
}
return $all_blocks;
}
/**
* Parses wp_template content and injects the current theme's
* stylesheet as a theme attribute into each wp_template_part
*
* @param string $template_content serialized wp_template content.
*
* @return string Updated wp_template content.
*/
public static function gutenberg_inject_theme_attribute_in_content( $template_content ) {
$has_updated_content = false;
$new_content = '';
$template_blocks = parse_blocks( $template_content );
$blocks = self::gutenberg_flatten_blocks( $template_blocks );
foreach ( $blocks as &$block ) {
if (
'core/template-part' === $block['blockName'] &&
! isset( $block['attrs']['theme'] )
) {
$block['attrs']['theme'] = wp_get_theme()->get_stylesheet();
$has_updated_content = true;
}
}
if ( $has_updated_content ) {
foreach ( $template_blocks as &$block ) {
$new_content .= serialize_block( $block );
}
return $new_content;
}
return $template_content;
}
/**
* Build a unified template object based a post Object.
*
* @param \WP_Post $post Template post.
*
* @return \WP_Block_Template|\WP_Error Template.
*/
public static function gutenberg_build_template_result_from_post( $post ) {
$terms = get_the_terms( $post, 'wp_theme' );
if ( is_wp_error( $terms ) ) {
return $terms;
}
if ( ! $terms ) {
return new \WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'woocommerce' ) );
}
$theme = $terms[0]->name;
$has_theme_file = true;
$template = new \WP_Block_Template();
$template->wp_id = $post->ID;
$template->id = $theme . '//' . $post->post_name;
$template->theme = $theme;
$template->content = $post->post_content;
$template->slug = $post->post_name;
$template->source = 'custom';
$template->type = $post->post_type;
$template->description = $post->post_excerpt;
$template->title = $post->post_title;
$template->status = $post->post_status;
$template->has_theme_file = $has_theme_file;
$template->is_custom = false;
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
if ( 'wp_template_part' === $post->post_type ) {
$type_terms = get_the_terms( $post, 'wp_template_part_area' );
if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
$template->area = $type_terms[0]->name;
}
}
// We are checking 'woocommerce' to maintain legacy templates which are saved to the DB,
// prior to updating to use the correct slug.
// More information found here: https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5423.
if ( self::PLUGIN_SLUG === $theme || 'woocommerce' === strtolower( $theme ) ) {
$template->origin = 'plugin';
}
return $template;
}
/**
* Build a unified template object based on a theme file.
*
* @param array|object $template_file Theme file.
* @param string $template_type wp_template or wp_template_part.
*
* @return \WP_Block_Template Template.
*/
public static function gutenberg_build_template_result_from_file( $template_file, $template_type ) {
$template_file = (object) $template_file;
// If the theme has an archive-products.html template but does not have product taxonomy templates
// then we will load in the archive-product.html template from the theme to use for product taxonomies on the frontend.
$template_is_from_theme = 'theme' === $template_file->source;
$theme_name = wp_get_theme()->get( 'TextDomain' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$template_content = file_get_contents( $template_file->path );
$template = new \WP_Block_Template();
$template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug;
$template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG;
$template->content = self::gutenberg_inject_theme_attribute_in_content( $template_content );
// Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
$template->source = $template_file->source ? $template_file->source : 'plugin';
$template->slug = $template_file->slug;
$template->type = $template_type;
$template->title = ! empty( $template_file->title ) ? $template_file->title : self::convert_slug_to_title( $template_file->slug );
$template->status = 'publish';
$template->has_theme_file = true;
$template->origin = $template_file->source;
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
$template->area = 'uncategorized';
return $template;
}
/**
* Build a new template object so that we can make Woo Blocks default templates available in the current theme should they not have any.
*
* @param string $template_file Block template file path.
* @param string $template_type wp_template or wp_template_part.
* @param string $template_slug Block template slug e.g. single-product.
* @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of Woo Blocks.
*
* @return object Block template object.
*/
public static function create_new_block_template_object( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) {
$theme_name = wp_get_theme()->get( 'TextDomain' );
$new_template_item = array(
'slug' => $template_slug,
'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug,
'path' => $template_file,
'type' => $template_type,
'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG,
// Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
'source' => $template_is_from_theme ? 'theme' : 'plugin',
'title' => self::convert_slug_to_title( $template_slug ),
'description' => '',
'post_types' => array(), // Don't appear in any Edit Post template selector dropdown.
);
return (object) $new_template_item;
}
/**
* Converts template slugs into readable titles.
*
* @param string $template_slug The templates slug (e.g. single-product).
* @return string Human friendly title converted from the slug.
*/
public static function convert_slug_to_title( $template_slug ) {
switch ( $template_slug ) {
case 'single-product':
return __( 'Single Product', 'woocommerce' );
case 'archive-product':
return __( 'Product Archive', 'woocommerce' );
case 'taxonomy-product_cat':
return __( 'Product Category', 'woocommerce' );
case 'taxonomy-product_tag':
return __( 'Product Tag', 'woocommerce' );
default:
// Replace all hyphens and underscores with spaces.
return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
}
}
/**
* Gets the first matching template part within themes directories
*
* Since [Gutenberg 12.1.0](https://github.com/WordPress/gutenberg/releases/tag/v12.1.0), the conventions for
* block templates and parts directory has changed from `block-templates` and `block-templates-parts`
* to `templates` and `parts` respectively.
*
* This function traverses all possible combinations of directory paths where a template or part
* could be located and returns the first one which is readable, prioritizing the new convention
* over the deprecated one, but maintaining that one for backwards compatibility.
*
* @param string $template_slug The slug of the template (i.e. without the file extension).
* @param string $template_type Either `wp_template` or `wp_template_part`.
*
* @return string|null The matched path or `null` if no match was found.
*/
public static function get_theme_template_path( $template_slug, $template_type = 'wp_template' ) {
$template_filename = $template_slug . '.html';
$possible_templates_dir = 'wp_template' === $template_type ? array(
self::DIRECTORY_NAMES['TEMPLATES'],
self::DIRECTORY_NAMES['DEPRECATED_TEMPLATES'],
) : array(
self::DIRECTORY_NAMES['TEMPLATE_PARTS'],
self::DIRECTORY_NAMES['DEPRECATED_TEMPLATE_PARTS'],
);
// Combine the possible root directory names with either the template directory
// or the stylesheet directory for child themes.
$possible_paths = array_reduce(
$possible_templates_dir,
function ( $carry, $item ) use ( $template_filename ) {
$filepath = DIRECTORY_SEPARATOR . $item . DIRECTORY_SEPARATOR . $template_filename;
$carry[] = get_template_directory() . $filepath;
$carry[] = get_stylesheet_directory() . $filepath;
return $carry;
},
array()
);
// Return the first matching.
foreach ( $possible_paths as $path ) {
if ( is_readable( $path ) ) {
return $path;
}
}
return null;
}
/**
* Check if the theme has a template. So we know if to load our own in or not.
*
* @param string $template_name name of the template file without .html extension e.g. 'single-product'.
* @return boolean
*/
public static function theme_has_template( $template_name ) {
return (bool) self::get_theme_template_path( $template_name, 'wp_template' );
}
/**
* Check if the theme has a template. So we know if to load our own in or not.
*
* @param string $template_name name of the template file without .html extension e.g. 'single-product'.
* @return boolean
*/
public static function theme_has_template_part( $template_name ) {
return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' );
}
/**
* Checks to see if they are using a compatible version of WP, or if not they have a compatible version of the Gutenberg plugin installed.
*
* @return boolean
*/
public static function supports_block_templates() {
if (
( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) &&
( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() )
) {
return false;
}
return true;
}
/**
* Returns whether the blockified templates should be used or not.
*
* First, we need to make sure WordPress version is higher than 6.1 (lowest that supports Products block).
* Then, if the option is not stored on the db, we need to check if the current theme is a block one or not.
*
* @return boolean
*/
public static function should_use_blockified_product_grid_templates() {
$minimum_wp_version = '6.1';
if ( version_compare( $GLOBALS['wp_version'], $minimum_wp_version, '<' ) ) {
return false;
}
$use_blockified_templates = wc_string_to_bool( get_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE ) );
if ( false === $use_blockified_templates ) {
return function_exists( 'wc_current_theme_is_fse_theme' ) && wc_current_theme_is_fse_theme();
}
return $use_blockified_templates;
}
}

View File

@ -0,0 +1,156 @@
<?php
declare( strict_types = 1);
//phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps
/**
* Utils for compatibility with WooCommerce Full Site Editor Blocks
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @version 9.4.0
*/
class WC_Brands_Block_Templates {
/**
* Constructor.
*/
public function __construct() {
add_action( 'get_block_templates', array( $this, 'get_block_templates' ), 10, 3 );
add_filter( 'get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
add_filter( 'woocommerce_has_block_template', array( $this, 'has_block_template' ), 10, 2 );
}
/**
* Get the taxonomy-product_brand template from DB in case a user customized it in FSE
*
* @return WP_Post|null The taxonomy-product_brand
*/
private function get_product_brand_template_db() {
$posts = get_posts(
array(
'name' => 'taxonomy-product_brand',
'post_type' => 'wp_template',
'post_status' => 'publish',
'posts_per_page' => 1,
)
);
if ( count( $posts ) ) {
return $posts[0];
}
return null;
}
/**
* Fixes a bug regarding taxonomies and FSE.
* Without this, the system will always load archive-product.php version instead of taxonomy_product_brand.html
* it will show a deprecation error if that happens.
*
* Triggered by woocommerce_has_block_template filter
*
* @param bool $has_template True if the template is available.
* @param string $template_name The name of the template.
*
* @return bool True if the system is checking archive-product
*/
public function has_block_template( $has_template, $template_name ) {
if ( 'archive-product' === $template_name || 'taxonomy-product_brand' === $template_name ) {
$has_template = true;
}
return $has_template;
}
/**
* Get the block template for Taxonomy Product Brand. First it attempts to load the last version from DB
* Otherwise it loads the file based template.
*
* @param string $template_type The post_type for the template. Normally wp_template or wp_template_part.
*
* @return WP_Block_Template The taxonomy-product_brand template.
*/
private function get_product_brands_template( $template_type ) {
$template_db = $this->get_product_brand_template_db();
if ( $template_db ) {
return BlockTemplateUtilsDuplicated::gutenberg_build_template_result_from_post( $template_db );
}
$template_path = BlockTemplateUtilsDuplicated::should_use_blockified_product_grid_templates()
? WC()->plugin_path() . '/templates/templates/blockified/taxonomy-product_brand.html'
: WC()->plugin_path() . '/templates/templates/taxonomy-product_brand.html';
$template_file = BlockTemplateUtilsDuplicated::create_new_block_template_object( $template_path, $template_type, 'taxonomy-product_brand', false );
return BlockTemplateUtilsDuplicated::gutenberg_build_template_result_from_file( $template_file, $template_type );
}
/**
* Function to check if a template name is woocommerce/taxonomy-product_brand
*
* Notice depending on the version of WooCommerce this could be:
*
* woocommerce//taxonomy-product_brand
* woocommerce/woocommerce//taxonomy-product_brand
*
* @param String $id The string to check if contains the template name.
*
* @return bool True if the template is woocommerce/taxonomy-product_brand
*/
private function is_taxonomy_product_brand_template( $id ) {
return strpos( $id, 'woocommerce//taxonomy-product_brand' ) !== false;
}
/**
* Get the block template for Taxonomy Product Brand if requested.
* Triggered by get_block_file_template action
*
* @param WP_Block_Template|null $block_template The current Block Template loaded, if any.
* @param string $id The template id normally in the format theme-slug//template-slug.
* @param string $template_type The post_type for the template. Normally wp_template or wp_template_part.
*
* @return WP_Block_Template|null The taxonomy-product_brand template.
*/
public function get_block_file_template( $block_template, $id, $template_type ) {
if ( $this->is_taxonomy_product_brand_template( $id ) && is_null( $block_template ) ) {
$block_template = $this->get_product_brands_template( $template_type );
}
return $block_template;
}
/**
* Add the Block template in the template query results needed by FSE
* Triggered by get_block_templates action
*
* @param array $query_result The list of templates to render in the query.
* @param array $query The current query parameters.
* @param string $template_type The post_type for the template. Normally wp_template or wp_template_part.
*
* @return WP_Block_Template[] Array of the matched Block Templates to render.
*/
public function get_block_templates( $query_result, $query, $template_type ) {
// We don't want to run this if we are looking for template-parts. Like the header.
if ( 'wp_template' !== $template_type ) {
return $query_result;
}
$post_id = isset( $_REQUEST['postId'] ) ? wc_clean( wp_unslash( $_REQUEST['postId'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$slugs = $query['slug__in'] ?? array();
// Only add the template if asking for Product Brands.
if (
in_array( 'taxonomy-product_brand', $slugs, true ) ||
( ! $post_id && ! count( $slugs ) ) ||
( ! count( $slugs ) && $this->is_taxonomy_product_brand_template( $post_id ) )
) {
$query_result[] = $this->get_product_brands_template( $template_type );
}
return $query_result;
}
}
new WC_Brands_Block_Templates();

View File

@ -77,6 +77,11 @@ class WC_Autoloader {
return;
}
// If the class is already loaded from a merged package, prevent autoloader from loading it as well.
if ( \Automattic\WooCommerce\Packages::should_load_class( $class ) ) {
return;
}
$file = $this->get_file_name_from_class( $class );
$path = '';

View File

@ -0,0 +1,68 @@
<?php
//phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
declare( strict_types = 1);
/**
* Brand settings manager.
*
* This class is responsible for setting and getting brand settings for a coupon.
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @version 9.4.0
*/
class WC_Brands_Brand_Settings_Manager {
/**
* Brand settings for a coupon.
*
* @var array
*/
private static $brand_settings = array();
/**
* Set brand settings for a coupon.
*
* @param WC_Coupon $coupon Coupon object.
*/
public static function set_brand_settings_on_coupon( $coupon ) {
$coupon_id = $coupon->get_id();
// Check if the brand settings are already set for this coupon.
if ( isset( self::$brand_settings[ $coupon_id ] ) ) {
return;
}
$included_brands = get_post_meta( $coupon_id, 'product_brands', true );
$included_brands = ! empty( $included_brands ) ? $included_brands : array();
$excluded_brands = get_post_meta( $coupon_id, 'exclude_product_brands', true );
$excluded_brands = ! empty( $excluded_brands ) ? $excluded_brands : array();
// Store these settings in the static array.
self::$brand_settings[ $coupon_id ] = array(
'included_brands' => $included_brands,
'excluded_brands' => $excluded_brands,
);
}
/**
* Get brand settings for a coupon.
*
* @param WC_Coupon $coupon Coupon object.
* @return array Brand settings (included and excluded brands).
*/
public static function get_brand_settings_on_coupon( $coupon ) {
$coupon_id = $coupon->get_id();
if ( isset( self::$brand_settings[ $coupon_id ] ) ) {
return self::$brand_settings[ $coupon_id ];
}
// Default return value if no settings are found.
return array(
'included_brands' => array(),
'excluded_brands' => array(),
);
}
}

View File

@ -0,0 +1,189 @@
<?php
declare( strict_types = 1);
/**
* WC_Brands_Coupons class.
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @version 9.4.0
*/
class WC_Brands_Coupons {
const E_WC_COUPON_EXCLUDED_BRANDS = 301;
/**
* Constructor
*/
public function __construct() {
// Coupon validation and error handling.
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'is_coupon_valid' ), 10, 3 );
add_filter( 'woocommerce_coupon_is_valid_for_product', array( $this, 'is_valid_for_product' ), 10, 3 );
add_filter( 'woocommerce_coupon_error', array( $this, 'brand_exclusion_error' ), 10, 2 );
}
/**
* Validate the coupon based on included and/or excluded product brands.
*
* If one of the following conditions are met, an exception will be thrown and
* displayed as an error notice on the cart page:
*
* 1) Coupon has a brand requirement but no products in the cart have the brand.
* 2) All products in the cart match the brand exclusion rule.
* 3) For a cart discount, there is at least one product in cart that matches exclusion rule.
*
* @throws Exception Throws Exception for invalid coupons.
* @param bool $valid Whether the coupon is valid.
* @param WC_Coupon $coupon Coupon object.
* @param WC_Discounts $discounts Discounts object.
* @return bool $valid True if coupon is valid, otherwise Exception will be thrown.
*/
public function is_coupon_valid( $valid, $coupon, $discounts = null ) {
$this->set_brand_settings_on_coupon( $coupon );
// Only check if coupon has brand restrictions on it.
$brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon );
$brand_restrictions = ! empty( $brand_coupon_settings['included_brands'] ) || ! empty( $brand_coupon_settings['excluded_brands'] );
if ( ! $brand_restrictions ) {
return $valid;
}
$included_brands_match = false;
$excluded_brands_matches = 0;
$items = $discounts->get_items();
foreach ( $items as $item ) {
$product_brands = $this->get_product_brands( $this->get_product_id( $item->product ) );
if ( ! empty( array_intersect( $product_brands, $brand_coupon_settings['included_brands'] ) ) ) {
$included_brands_match = true;
}
if ( ! empty( array_intersect( $product_brands, $brand_coupon_settings['excluded_brands'] ) ) ) {
++$excluded_brands_matches;
}
}
// 1) Coupon has a brand requirement but no products in the cart have the brand.
if ( ! $included_brands_match && ! empty( $brand_coupon_settings['included_brands'] ) ) {
throw new Exception( WC_Coupon::E_WC_COUPON_NOT_APPLICABLE ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
// 2) All products in the cart match brand exclusion rule.
if ( count( $items ) === $excluded_brands_matches ) {
throw new Exception( self::E_WC_COUPON_EXCLUDED_BRANDS ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
// 3) For a cart discount, there is at least one product in cart that matches exclusion rule.
if ( $coupon->is_type( 'fixed_cart' ) && $excluded_brands_matches > 0 ) {
throw new Exception( self::E_WC_COUPON_EXCLUDED_BRANDS ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return $valid;
}
/**
* Check if a coupon is valid for a product.
*
* This allows percentage and product discounts to apply to only
* the correct products in the cart.
*
* @param bool $valid Whether the product should get the coupon's discounts.
* @param WC_Product $product WC Product Object.
* @param WC_Coupon $coupon Coupon object.
* @return bool $valid
*/
public function is_valid_for_product( $valid, $product, $coupon ) {
if ( ! is_a( $product, 'WC_Product' ) ) {
return $valid;
}
$this->set_brand_settings_on_coupon( $coupon );
$product_id = $this->get_product_id( $product );
$product_brands = $this->get_product_brands( $product_id );
// Check if coupon has a brand requirement and if this product has that brand attached.
$brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon );
if ( ! empty( $brand_coupon_settings['included_brands'] ) && empty( array_intersect( $product_brands, $brand_coupon_settings['included_brands'] ) ) ) {
return false;
}
// Check if coupon has a brand exclusion and if this product has that brand attached.
if ( ! empty( $brand_coupon_settings['excluded_brands'] ) && ! empty( array_intersect( $product_brands, $brand_coupon_settings['excluded_brands'] ) ) ) {
return false;
}
return $valid;
}
/**
* Display a custom error message when a cart discount coupon does not validate
* because an excluded brand was found in the cart.
*
* @param string $err The error message.
* @param string $err_code The error code.
* @return string
*/
public function brand_exclusion_error( $err, $err_code ) {
if ( self::E_WC_COUPON_EXCLUDED_BRANDS !== $err_code ) {
return $err;
}
return __( 'Sorry, this coupon is not applicable to the brands of selected products.', 'woocommerce' );
}
/**
* Get a list of brands that are assigned to a specific product
*
* @param int $product_id Product id.
* @return array brands
*/
private function get_product_brands( $product_id ) {
return wp_get_post_terms( $product_id, 'product_brand', array( 'fields' => 'ids' ) );
}
/**
* Set brand settings as properties on coupon object. These properties are
* lists of included product brand IDs and list of excluded brand IDs.
*
* @param WC_Coupon $coupon Coupon object.
*
* @return void
*/
private function set_brand_settings_on_coupon( $coupon ) {
$brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon );
if ( ! empty( $brand_coupon_settings['included_brands'] ) && ! empty( $brand_coupon_settings['excluded_brands'] ) ) {
return;
}
$included_brands = get_post_meta( $coupon->get_id(), 'product_brands', true );
if ( empty( $included_brands ) ) {
$included_brands = array();
}
$excluded_brands = get_post_meta( $coupon->get_id(), 'exclude_product_brands', true );
if ( empty( $excluded_brands ) ) {
$excluded_brands = array();
}
// Store these for later to avoid multiple look-ups.
WC_Brands_Brand_Settings_Manager::set_brand_settings_on_coupon( $coupon );
}
/**
* Returns the product (or variant) ID.
*
* @param WC_Product $product WC Product Object.
* @return int Product ID
*/
private function get_product_id( $product ) {
return $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
}
}
new WC_Brands_Coupons();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
<?php
/**
* REST API Brands controller for WC 3.5+
*
* Handles requests to /products/brands endpoint.
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce\RestApi
* @since 9.4.0
*/
declare( strict_types = 1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Brands controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Categories_Controller
*/
class WC_REST_Product_Brands_V2_Controller extends WC_REST_Product_Categories_V2_Controller {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/brands';
/**
* Taxonomy.
*
* @var string
*/
protected $taxonomy = 'product_brand';
}

View File

@ -0,0 +1,39 @@
<?php
/**
* REST API Brands controller.
*
* Handles requests to /products/brands endpoint.
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce\RestApi
* @since 9.4.0
*/
declare( strict_types = 1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Brands controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Categories_Controller
*/
class WC_REST_Product_Brands_Controller extends WC_REST_Product_Categories_Controller {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/brands';
/**
* Taxonomy.
*
* @var string
*/
protected $taxonomy = 'product_brand';
}

View File

@ -0,0 +1,141 @@
<?php
/**
* Brands Helper Functions
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce
* @version 9.4.0
*/
declare( strict_types = 1);
/**
* Helper function :: wc_get_brand_thumbnail_url function.
*
* @param int $brand_id Brand ID.
* @param string $size Thumbnail image size.
* @return string
*/
function wc_get_brand_thumbnail_url( $brand_id, $size = 'full' ) {
$thumbnail_id = get_term_meta( $brand_id, 'thumbnail_id', true );
if ( $thumbnail_id ) {
$thumb_src = wp_get_attachment_image_src( $thumbnail_id, $size );
}
return ! empty( $thumb_src ) ? current( $thumb_src ) : '';
}
/**
* Helper function :: wc_get_brand_thumbnail_image function.
*
* @since 9.4.0
*
* @param object $brand Brand term.
* @param string $size Thumbnail image size.
* @return string
*/
function wc_get_brand_thumbnail_image( $brand, $size = '' ) {
$thumbnail_id = get_term_meta( $brand->term_id, 'thumbnail_id', true );
if ( '' === $size || 'brand-thumb' === $size ) {
/**
* Filter the brand's thumbnail size.
*
* @since 9.4.0
*
* @param string $size Brand's thumbnail size.
*/
$size = apply_filters( 'woocommerce_brand_thumbnail_size', 'shop_catalog' );
}
if ( $thumbnail_id ) {
$image_src = wp_get_attachment_image_src( $thumbnail_id, $size );
$image_src = $image_src[0];
$dimensions = wc_get_image_size( $size );
$image_srcset = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $thumbnail_id, $size ) : false;
$image_sizes = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $thumbnail_id, $size ) : false;
} else {
$image_src = wc_placeholder_img_src();
$dimensions = wc_get_image_size( $size );
$image_srcset = false;
$image_sizes = false;
}
// Add responsive image markup if available.
if ( $image_srcset && $image_sizes ) {
$image = '<img src="' . esc_url( $image_src ) . '" alt="' . esc_attr( $brand->name ) . '" class="brand-thumbnail" width="' . esc_attr( $dimensions['width'] ) . '" height="' . esc_attr( $dimensions['height'] ) . '" srcset="' . esc_attr( $image_srcset ) . '" sizes="' . esc_attr( $image_sizes ) . '" />';
} else {
$image = '<img src="' . esc_url( $image_src ) . '" alt="' . esc_attr( $brand->name ) . '" class="brand-thumbnail" width="' . esc_attr( $dimensions['width'] ) . '" height="' . esc_attr( $dimensions['height'] ) . '" />';
}
return $image;
}
/**
* Retrieves product's brands.
*
* @param int $post_id Post ID (default: 0).
* @param string $sep Seperator (default: ').
* @param string $before Before item (default: '').
* @param string $after After item (default: '').
* @return array List of terms
*/
function wc_get_brands( $post_id = 0, $sep = ', ', $before = '', $after = '' ) {
global $post;
if ( ! $post_id ) {
$post_id = $post->ID;
}
return get_the_term_list( $post_id, 'product_brand', $before, $sep, $after );
}
/**
* Polyfills for backwards compatibility with the WooCommerce Brands plugin.
*/
if ( ! function_exists( 'get_brand_thumbnail_url' ) ) {
/**
* Polyfill for get_brand_thumbnail_image.
*
* @param int $brand_id Brand ID.
* @param string $size Thumbnail image size.
* @return string
*/
function get_brand_thumbnail_url( $brand_id, $size = 'full' ) {
return wc_get_brand_thumbnail_url( $brand_id, $size );
}
}
if ( ! function_exists( 'get_brand_thumbnail_image' ) ) {
/**
* Polyfill for get_brand_thumbnail_image.
*
* @param object $brand Brand term.
* @param string $size Thumbnail image size.
* @return string
*/
function get_brand_thumbnail_image( $brand, $size = '' ) {
return wc_get_brand_thumbnail_image( $brand, $size );
}
}
if ( ! function_exists( 'get_brands' ) ) {
/**
* Polyfill for get_brands.
*
* @param int $post_id Post ID (default: 0).
* @param string $sep Seperator (default: ').
* @param string $before Before item (default: '').
* @param string $after After item (default: '').
* @return array List of terms
*/
function get_brands( $post_id = 0, $sep = ', ', $before = '', $after = '' ) {
return wc_get_brands( $post_id, $sep, $before, $after );
}
}

View File

@ -0,0 +1,130 @@
<?php
declare( strict_types = 1);
/**
* Brand Description Widget
*
* When viewing a brand archive, show the current brands description + image
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce\Widgets
* @version 9.4.0
*/
class WC_Widget_Brand_Description extends WP_Widget {
/**
* Widget class.
*
* @var string
*/
public $woo_widget_cssclass;
/**
* Widget description.
*
* @var string
*/
public $woo_widget_description;
/**
* Widget idbase.
*
* @var string
*/
public $woo_widget_idbase;
/**
* Widget name.
*
* @var string
*/
public $woo_widget_name;
/** Constructor */
public function __construct() {
/* Widget variable settings. */
$this->woo_widget_name = __( 'WooCommerce Brand Description', 'woocommerce' );
$this->woo_widget_description = __( 'When viewing a brand archive, show the current brands description.', 'woocommerce' );
$this->woo_widget_idbase = 'wc_brands_brand_description';
$this->woo_widget_cssclass = 'widget_brand_description';
/* Widget settings. */
$widget_ops = array(
'classname' => $this->woo_widget_cssclass,
'description' => $this->woo_widget_description,
);
/* Create the widget. */
parent::__construct( $this->woo_widget_idbase, $this->woo_widget_name, $widget_ops );
}
/**
* Echoes the widget content.
*
* @see WP_Widget
*
* @param array $args Display arguments including 'before_title', 'after_title',
* 'before_widget', and 'after_widget'.
* @param array $instance The settings for the particular instance of the widget.
*/
public function widget( $args, $instance ) {
extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
if ( ! is_tax( 'product_brand' ) ) {
return;
}
if ( ! get_query_var( 'term' ) ) {
return;
}
$thumbnail = '';
$term = get_term_by( 'slug', get_query_var( 'term' ), 'product_brand' );
$thumbnail = wc_get_brand_thumbnail_url( $term->term_id, 'large' );
echo $before_widget . $before_title . $term->name . $after_title; // phpcs:ignore WordPress.Security.EscapeOutput
wc_get_template(
'widgets/brand-description.php',
array(
'thumbnail' => $thumbnail,
'brand' => $term,
),
'woocommerce',
WC()->plugin_path() . '/templates/brands/'
);
echo $after_widget; // phpcs:ignore WordPress.Security.EscapeOutput
}
/**
* Updates widget instance.
*
* @see WP_Widget->update
*
* @param array $new_instance New widget instance.
* @param array $old_instance Old widget instance.
*/
public function update( $new_instance, $old_instance ) {
$instance['title'] = wp_strip_all_tags( stripslashes( $new_instance['title'] ) );
return $instance;
}
/**
* Outputs the settings update form.
*
* @param array $instance Current settings.
*/
public function form( $instance ) {
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'woocommerce' ); ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" value="<?php echo isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : ''; ?>" />
</p>
<?php
}
}

View File

@ -0,0 +1,531 @@
<?php
declare( strict_types = 1);
/**
* Layered Navigation Widget for brands WC 2.6 version
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce\Widgets
* @version 9.4.0
* @extends WP_Widget
*/
class WC_Widget_Brand_Nav extends WC_Widget {
/**
* Constructor
*
* @return void
*/
public function __construct() {
/* Widget variable settings. */
$this->widget_cssclass = 'woocommerce widget_brand_nav widget_layered_nav';
$this->widget_description = __( 'Shows brands in a widget which lets you narrow down the list of products when viewing products.', 'woocommerce' );
$this->widget_id = 'woocommerce_brand_nav';
$this->widget_name = __( 'WooCommerce Brand Layered Nav', 'woocommerce' );
add_filter( 'woocommerce_product_subcategories_args', array( $this, 'filter_out_cats' ) );
/* Create the widget. */
parent::__construct();
}
/**
* Filter out all categories and not display them
*
* @param array $cat_args Category arguments.
*/
public function filter_out_cats( $cat_args ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['filter_product_brand'] ) ) {
return array( 'taxonomy' => '' );
}
return $cat_args;
}
/**
* Return the currently viewed taxonomy name.
*
* @return string
*/
protected function get_current_taxonomy() {
return is_tax() ? get_queried_object()->taxonomy : '';
}
/**
* Return the currently viewed term ID.
*
* @return int
*/
protected function get_current_term_id() {
return absint( is_tax() ? get_queried_object()->term_id : 0 );
}
/**
* Return the currently viewed term slug.
*
* @return int
*/
protected function get_current_term_slug() {
return absint( is_tax() ? get_queried_object()->slug : 0 );
}
/**
* Widget function.
*
* @see WP_Widget
*
* @param array $args Arguments.
* @param array $instance Widget instance.
* @return void
*/
public function widget( $args, $instance ) {
$attribute_array = array();
$attribute_taxonomies = wc_get_attribute_taxonomies();
if ( ! empty( $attribute_taxonomies ) ) {
foreach ( $attribute_taxonomies as $tax ) {
if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
$attribute_array[ $tax->attribute_name ] = $tax->attribute_name;
}
}
}
if ( ! is_post_type_archive( 'product' ) && ! is_tax( array_merge( is_array( $attribute_array ) ? $attribute_array : array(), array( 'product_cat', 'product_tag' ) ) ) ) {
return;
}
$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
$current_term = $attribute_array && is_tax( $attribute_array ) ? get_queried_object()->term_id : '';
$current_tax = $attribute_array && is_tax( $attribute_array ) ? get_queried_object()->taxonomy : '';
/**
* Filter the widget's title.
*
* @since 9.4.0
*
* @param string $title Widget title
* @param array $instance The settings for the particular instance of the widget.
* @param string $woo_widget_idbase The widget's id base.
*/
$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
$taxonomy = 'product_brand';
$display_type = isset( $instance['display_type'] ) ? $instance['display_type'] : 'list';
if ( ! taxonomy_exists( $taxonomy ) ) {
return;
}
// Get only parent terms. Methods will recursively retrieve children.
$terms = get_terms(
array(
'taxonomy' => $taxonomy,
'hide_empty' => true,
'parent' => 0,
)
);
if ( empty( $terms ) ) {
return;
}
ob_start();
$this->widget_start( $args, $instance );
if ( 'dropdown' === $display_type ) {
$found = $this->layered_nav_dropdown( $terms, $taxonomy );
} else {
$found = $this->layered_nav_list( $terms, $taxonomy );
}
$this->widget_end( $args );
// Force found when option is selected - do not force found on taxonomy attributes.
if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) {
$found = true;
}
if ( ! $found ) {
ob_end_clean();
} else {
echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput
}
}
/**
* Update function.
*
* @see WP_Widget->update
*
* @param array $new_instance The new settings for the particular instance of the widget.
* @param array $old_instance The old settings for the particular instance of the widget.
* @return array
*/
public function update( $new_instance, $old_instance ) {
global $woocommerce;
if ( empty( $new_instance['title'] ) ) {
$new_instance['title'] = __( 'Brands', 'woocommerce' );
}
$instance['title'] = wp_strip_all_tags( stripslashes( $new_instance['title'] ) );
$instance['display_type'] = stripslashes( $new_instance['display_type'] );
return $instance;
}
/**
* Form function.
*
* @see WP_Widget->form
*
* @param array $instance Widget instance.
* @return void
*/
public function form( $instance ) {
global $woocommerce;
if ( ! isset( $instance['display_type'] ) ) {
$instance['display_type'] = 'list';
}
?>
<p><label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'woocommerce' ); ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" value="<?php echo isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : ''; ?>" />
</p>
<p><label for="<?php echo esc_attr( $this->get_field_id( 'display_type' ) ); ?>"><?php esc_html_e( 'Display Type:', 'woocommerce' ); ?></label>
<select id="<?php echo esc_attr( $this->get_field_id( 'display_type' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'display_type' ) ); ?>">
<option value="list" <?php selected( $instance['display_type'], 'list' ); ?>><?php esc_html_e( 'List', 'woocommerce' ); ?></option>
<option value="dropdown" <?php selected( $instance['display_type'], 'dropdown' ); ?>><?php esc_html_e( 'Dropdown', 'woocommerce' ); ?></option>
</select></p>
<?php
}
/**
* Get current page URL for layered nav items.
*
* @param string $taxonomy Taxonomy.
* @return string
*/
protected function get_page_base_url( $taxonomy ) {
if ( defined( 'SHOP_IS_ON_FRONT' ) ) {
$link = home_url();
} elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) {
$link = get_post_type_archive_link( 'product' );
} elseif ( is_product_category() ) {
$link = get_term_link( get_query_var( 'product_cat' ), 'product_cat' );
} elseif ( is_product_tag() ) {
$link = get_term_link( get_query_var( 'product_tag' ), 'product_tag' );
} else {
$link = get_term_link( get_query_var( 'term' ), get_query_var( 'taxonomy' ) );
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Min/Max.
if ( isset( $_GET['min_price'] ) ) {
$link = add_query_arg( 'min_price', wc_clean( wp_unslash( $_GET['min_price'] ) ), $link );
}
if ( isset( $_GET['max_price'] ) ) {
$link = add_query_arg( 'max_price', wc_clean( wp_unslash( $_GET['max_price'] ) ), $link );
}
// Orderby.
if ( isset( $_GET['orderby'] ) ) {
$link = add_query_arg( 'orderby', wc_clean( wp_unslash( $_GET['orderby'] ) ), $link );
}
/**
* Search Arg.
* To support quote characters, first they are decoded from &quot; entities, then URL encoded.
*/
if ( get_search_query() ) {
$link = add_query_arg( 's', rawurlencode( htmlspecialchars_decode( get_search_query() ) ), $link );
}
// Post Type Arg.
if ( isset( $_GET['post_type'] ) ) {
$link = add_query_arg( 'post_type', wc_clean( wp_unslash( $_GET['post_type'] ) ), $link );
}
// Min Rating Arg.
if ( isset( $_GET['min_rating'] ) ) {
$link = add_query_arg( 'min_rating', wc_clean( wp_unslash( $_GET['min_rating'] ) ), $link );
}
// All current filters.
$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
if ( $_chosen_attributes ) {
foreach ( $_chosen_attributes as $name => $data ) {
if ( $name === $taxonomy ) {
continue;
}
$filter_name = sanitize_title( str_replace( 'pa_', '', $name ) );
if ( ! empty( $data['terms'] ) ) {
$link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link );
}
if ( 'or' === $data['query_type'] ) {
$link = add_query_arg( 'query_type_' . $filter_name, 'or', $link );
}
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return esc_url( $link );
}
/**
* Gets the currently selected attributes
*
* @return array
*/
public function get_chosen_attributes() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['filter_product_brand'] ) ) {
$filter_product_brand = wc_clean( wp_unslash( $_GET['filter_product_brand'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return array_map( 'intval', explode( ',', $filter_product_brand ) );
}
return array();
}
/**
* Show dropdown layered nav.
*
* @param array $terms Terms.
* @param string $taxonomy Taxonomy.
* @param int $depth Depth.
* @return bool Will nav display?
*/
protected function layered_nav_dropdown( $terms, $taxonomy, $depth = 0 ) {
$found = false;
if ( $taxonomy !== $this->get_current_taxonomy() ) {
$term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, 'or' );
$_chosen_attributes = $this->get_chosen_attributes();
if ( 0 === $depth ) {
echo '<select class="wc-brand-dropdown-layered-nav-' . esc_attr( $taxonomy ) . '">';
echo '<option value="">' . esc_html__( 'Any Brand', 'woocommerce' ) . '</option>';
}
foreach ( $terms as $term ) {
// If on a term page, skip that term in widget list.
if ( $term->term_id === $this->get_current_term_id() ) {
continue;
}
// Get count based on current view.
$current_values = ! empty( $_chosen_attributes ) ? $_chosen_attributes : array();
$option_is_set = in_array( $term->term_id, $current_values, true );
$count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0;
// Only show options with count > 0.
if ( 0 < $count ) {
$found = true;
} elseif ( 0 === $count && ! $option_is_set ) {
continue;
}
echo '<option value="' . esc_attr( $term->term_id ) . '" ' . selected( $option_is_set, true, false ) . '>' . esc_html( str_repeat( '&nbsp;', 2 * $depth ) . $term->name ) . '</option>';
$child_terms = get_terms(
array(
'taxonomy' => $taxonomy,
'hide_empty' => true,
'parent' => $term->term_id,
)
);
if ( ! empty( $child_terms ) ) {
$found |= $this->layered_nav_dropdown( $child_terms, $taxonomy, $depth + 1 );
}
}
if ( 0 === $depth ) {
$link = $this->get_page_base_url( $taxonomy );
echo '</select>';
wc_enqueue_js(
"
jQuery( '.wc-brand-dropdown-layered-nav-" . esc_js( $taxonomy ) . "' ).change( function() {
var slug = jQuery( this ).val();
location.href = '" . preg_replace( '%\/page\/[0-9]+%', '', str_replace( array( '&amp;', '%2C' ), array( '&', ',' ), esc_js( add_query_arg( 'filtering', '1', $link ) ) ) ) . '&filter_' . esc_js( $taxonomy ) . "=' + jQuery( '.wc-brand-dropdown-layered-nav-" . esc_js( $taxonomy ) . "' ).val();
});
"
);
}
}
return $found;
}
/**
* Show list based layered nav.
*
* @param array $terms Terms.
* @param string $taxonomy Taxonomy.
* @param int $depth Depth.
* @return bool Will nav display?
*/
protected function layered_nav_list( $terms, $taxonomy, $depth = 0 ) {
// List display.
echo '<ul class="' . ( 0 === $depth ? '' : 'children ' ) . 'wc-brand-list-layered-nav-' . esc_attr( $taxonomy ) . '">';
$term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, 'or' );
$_chosen_attributes = $this->get_chosen_attributes();
$current_values = ! empty( $_chosen_attributes ) ? $_chosen_attributes : array();
$found = false;
$filter_name = 'filter_' . $taxonomy;
foreach ( $terms as $term ) {
$option_is_set = in_array( $term->term_id, $current_values, true );
$count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0;
// skip the term for the current archive.
if ( $this->get_current_term_id() === $term->term_id ) {
continue;
}
// Only show options with count > 0.
if ( 0 < $count ) {
$found = true;
} elseif ( 0 === $count && ! $option_is_set ) {
continue;
}
$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_filter = array_map( 'intval', $current_filter );
if ( ! in_array( $term->term_id, $current_filter, true ) ) {
$current_filter[] = $term->term_id;
}
$link = $this->get_page_base_url( $taxonomy );
// Add current filters to URL.
foreach ( $current_filter as $key => $value ) {
// Exclude query arg for current term archive term.
if ( $value === $this->get_current_term_id() ) {
unset( $current_filter[ $key ] );
}
// Exclude self so filter can be unset on click.
if ( $option_is_set && $value === $term->term_id ) {
unset( $current_filter[ $key ] );
}
}
if ( ! empty( $current_filter ) ) {
$link = add_query_arg(
array(
'filtering' => '1',
$filter_name => implode( ',', $current_filter ),
),
$link
);
}
echo '<li class="wc-layered-nav-term ' . ( $option_is_set ? 'chosen' : '' ) . '">';
echo ( $count > 0 || $option_is_set ) ? '<a href="' . esc_url( apply_filters( 'woocommerce_layered_nav_link', $link ) ) . '">' : '<span>'; // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
echo esc_html( $term->name );
echo ( $count > 0 || $option_is_set ) ? '</a> ' : '</span> ';
echo wp_kses_post( apply_filters( 'woocommerce_layered_nav_count', '<span class="count">(' . absint( $count ) . ')</span>', $count, $term ) );// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$child_terms = get_terms(
array(
'taxonomy' => $taxonomy,
'hide_empty' => true,
'parent' => $term->term_id,
)
);
if ( ! empty( $child_terms ) ) {
$found |= $this->layered_nav_list( $child_terms, $taxonomy, $depth + 1 );
}
echo '</li>';
}
echo '</ul>';
return $found;
}
/**
* Count products within certain terms, taking the main WP query into consideration.
*
* @param array $term_ids Term IDs.
* @param string $taxonomy Taxonomy.
* @param string $query_type Query type.
* @return array
*/
protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type = 'and' ) {
global $wpdb;
$tax_query = WC_Query::get_main_tax_query();
$meta_query = WC_Query::get_main_meta_query();
if ( 'or' === $query_type ) {
foreach ( $tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
unset( $tax_query[ $key ] );
}
}
}
$meta_query = new WP_Meta_Query( $meta_query );
$tax_query = new WP_Tax_Query( $tax_query );
$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
$tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' );
// Generate query.
$query = array();
$query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) as term_count, terms.term_id as term_count_id";
$query['from'] = "FROM {$wpdb->posts}";
$query['join'] = "
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
" . $tax_query_sql['join'] . $meta_query_sql['join'];
$query['where'] = "
WHERE {$wpdb->posts}.post_type IN ( 'product' )
AND {$wpdb->posts}.post_status = 'publish'
" . $tax_query_sql['where'] . $meta_query_sql['where'] . '
AND terms.term_id IN (' . implode( ',', array_map( 'absint', $term_ids ) ) . ')
';
$query['group_by'] = 'GROUP BY terms.term_id';
$query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$query = implode( ' ', $query );
// We have a query - let's see if cached results of this query already exist.
$query_hash = md5( $query );
$cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
if ( true === $cache ) {
$cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) );
} else {
$cached_counts = array();
}
if ( ! isset( $cached_counts[ $query_hash ] ) ) {
$results = $wpdb->get_results( $query, ARRAY_A ); // @codingStandardsIgnoreLine
$counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
$cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) {
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, HOUR_IN_SECONDS );
}
}
return array_map( 'absint', (array) $cached_counts[ $query_hash ] );
}
}

View File

@ -0,0 +1,235 @@
<?php
declare( strict_types = 1);
/**
* Brand Thumbnails Widget
*
* Show brand images as thumbnails
*
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
*
* @package WooCommerce\Widgets
* @version 9.4.0
*/
class WC_Widget_Brand_Thumbnails extends WP_Widget {
/**
* Widget CSS class.
*
* @var string
*/
public $woo_widget_cssclass;
/**
* Widget description.
*
* @var string
*/
public $woo_widget_description;
/**
* Widget id base.
*
* @var string
*/
public $woo_widget_idbase;
/**
* Widget name.
*
* @var string
*/
public $woo_widget_name;
/** Constructor */
public function __construct() {
/* Widget variable settings. */
$this->woo_widget_name = __( 'WooCommerce Brand Thumbnails', 'woocommerce' );
$this->woo_widget_description = __( 'Show a grid of brand thumbnails.', 'woocommerce' );
$this->woo_widget_idbase = 'wc_brands_brand_thumbnails';
$this->woo_widget_cssclass = 'widget_brand_thumbnails';
/* Widget settings. */
$widget_ops = array(
'classname' => $this->woo_widget_cssclass,
'description' => $this->woo_widget_description,
);
/* Create the widget. */
parent::__construct( $this->woo_widget_idbase, $this->woo_widget_name, $widget_ops );
}
/**
* Echoes the widget content.
*
* @see WP_Widget
*
* @param array $args Display arguments including 'before_title', 'after_title',
* 'before_widget', and 'after_widget'.
* @param array $instance The settings for the particular instance of the widget.
*/
public function widget( $args, $instance ) {
$instance = wp_parse_args(
$instance,
array(
'title' => '',
'columns' => 1,
'exclude' => '',
'orderby' => 'name',
'hide_empty' => 0,
'number' => '',
)
);
$exclude = array_map( 'intval', explode( ',', $instance['exclude'] ) );
$order = 'name' === $instance['orderby'] ? 'asc' : 'desc';
$brands = get_terms(
array(
'taxonomy' => 'product_brand',
'hide_empty' => $instance['hide_empty'],
'orderby' => $instance['orderby'],
'exclude' => $exclude,
'number' => $instance['number'],
'order' => $order,
)
);
if ( ! $brands ) {
return;
}
/**
* Filter the widget's title.
*
* @since 9.4.0
*
* @param string $title Widget title
* @param array $instance The settings for the particular instance of the widget.
* @param string $woo_widget_idbase The widget's id base.
*/
$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->woo_widget_idbase );
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput
if ( '' !== $title ) {
echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput
}
wc_get_template(
'widgets/brand-thumbnails.php',
array(
'brands' => $brands,
'columns' => (int) $instance['columns'],
'fluid_columns' => ! empty( $instance['fluid_columns'] ) ? true : false,
),
'woocommerce',
WC()->plugin_path() . '/templates/brands/'
);
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput
}
/**
* Update widget instance.
*
* @param array $new_instance The new settings for the particular instance of the widget.
* @param array $old_instance The old settings for the particular instance of the widget.
*
* @see WP_Widget->update
*/
public function update( $new_instance, $old_instance ) {
$instance['title'] = wp_strip_all_tags( stripslashes( $new_instance['title'] ) );
$instance['columns'] = wp_strip_all_tags( stripslashes( $new_instance['columns'] ) );
$instance['fluid_columns'] = ! empty( $new_instance['fluid_columns'] ) ? true : false;
$instance['orderby'] = wp_strip_all_tags( stripslashes( $new_instance['orderby'] ) );
$instance['exclude'] = wp_strip_all_tags( stripslashes( $new_instance['exclude'] ) );
$instance['hide_empty'] = wp_strip_all_tags( stripslashes( (string) $new_instance['hide_empty'] ) );
$instance['number'] = wp_strip_all_tags( stripslashes( $new_instance['number'] ) );
if ( ! $instance['columns'] ) {
$instance['columns'] = 1;
}
if ( ! $instance['orderby'] ) {
$instance['orderby'] = 'name';
}
if ( ! $instance['exclude'] ) {
$instance['exclude'] = '';
}
if ( ! $instance['hide_empty'] ) {
$instance['hide_empty'] = 0;
}
if ( ! $instance['number'] ) {
$instance['number'] = '';
}
return $instance;
}
/**
* Outputs the settings update form.
*
* @param array $instance Current settings.
*/
public function form( $instance ) {
if ( ! isset( $instance['hide_empty'] ) ) {
$instance['hide_empty'] = 0;
}
if ( ! isset( $instance['orderby'] ) ) {
$instance['orderby'] = 'name';
}
if ( empty( $instance['fluid_columns'] ) ) {
$instance['fluid_columns'] = false;
}
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'woocommerce' ); ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" value="<?php echo isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : ''; ?>" />
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'columns' ) ); ?>"><?php esc_html_e( 'Columns:', 'woocommerce' ); ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'columns' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'columns' ) ); ?>" value="<?php echo isset( $instance['columns'] ) ? esc_attr( $instance['columns'] ) : '1'; ?>" />
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'fluid_columns' ) ); ?>"><?php esc_html_e( 'Fluid columns:', 'woocommerce' ); ?></label>
<input type="checkbox" <?php checked( $instance['fluid_columns'] ); ?> id="<?php echo esc_attr( $this->get_field_id( 'fluid_columns' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'fluid_columns' ) ); ?>" />
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'number' ) ); ?>"><?php esc_html_e( 'Number:', 'woocommerce' ); ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'number' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'number' ) ); ?>" value="<?php if ( isset ( $instance['number'] ) ) { echo esc_attr( $instance['number'] ); } // phpcs:ignore ?>" placeholder="<?php esc_attr_e( 'All', 'woocommerce' ); ?>" />
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'exclude' ) ); ?>"><?php esc_html_e( 'Exclude:', 'woocommerce' ); ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'exclude' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'exclude' ) ); ?>" value="<?php if ( isset ( $instance['exclude'] ) ) { echo esc_attr( $instance['exclude'] ); } // phpcs:ignore ?>" placeholder="<?php esc_attr_e( 'None', 'woocommerce' ); ?>" />
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'hide_empty' ) ); ?>"><?php esc_html_e( 'Hide empty brands:', 'woocommerce' ); ?></label>
<select id="<?php echo esc_attr( $this->get_field_id( 'hide_empty' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'hide_empty' ) ); ?>">
<option value="1" <?php selected( $instance['hide_empty'], 1 ); ?>><?php esc_html_e( 'Yes', 'woocommerce' ); ?></option>
<option value="0" <?php selected( $instance['hide_empty'], 0 ); ?>><?php esc_html_e( 'No', 'woocommerce' ); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>"><?php esc_html_e( 'Order by:', 'woocommerce' ); ?></label>
<select id="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'orderby' ) ); ?>">
<option value="name" <?php selected( $instance['orderby'], 'name' ); ?>><?php esc_html_e( 'Name', 'woocommerce' ); ?></option>
<option value="count" <?php selected( $instance['orderby'], 'count' ); ?>><?php esc_html_e( 'Count', 'woocommerce' ); ?></option>
</select>
</p>
<?php
}
}

View File

@ -0,0 +1,61 @@
<?php
/**
* Brands class file.
*/
declare( strict_types = 1);
namespace Automattic\WooCommerce\Internal;
defined( 'ABSPATH' ) || exit;
/**
* Class to initiate Brands functionality in core.
*/
class Brands {
/**
* Class initialization
*
* @internal
*/
final public static function init() {
if ( ! self::is_enabled() ) {
return;
}
// If the WooCommerce Brands plugin is activated via the WP CLI using the '--skip-plugins' flag, deactivate it here.
if ( function_exists( 'wc_brands_init' ) ) {
remove_action( 'plugins_loaded', 'wc_brands_init', 1 );
}
include_once WC_ABSPATH . 'includes/class-wc-brands.php';
include_once WC_ABSPATH . 'includes/class-wc-brands-coupons.php';
include_once WC_ABSPATH . 'includes/class-wc-brands-brand-settings-manager.php';
include_once WC_ABSPATH . 'includes/wc-brands-functions.php';
if ( wc_current_theme_is_fse_theme() ) {
include_once WC_ABSPATH . 'includes/blocks/class-wc-brands-block-templates.php';
include_once WC_ABSPATH . 'includes/blocks/class-wc-brands-block-template-utils-duplicated.php';
}
if ( is_admin() ) {
include_once WC_ABSPATH . 'includes/admin/class-wc-admin-brands.php';
}
}
/**
* Ensures that the Brands feature is released initially only to 5% of users.
*
* @return bool
*/
public static function is_enabled() {
$assignment = get_option( 'woocommerce_remote_variant_assignment', false );
if ( false === $assignment ) {
return false;
}
return ( $assignment <= 6 ); // Considering 5% of the 0-120 range.
}
}

View File

@ -37,23 +37,43 @@ class Packages {
* initialization for the now-merged feature plugin.
*
* Once a package has been merged into WooCommerce Core it should have its slug added here. This will ensure
* that we deactivate the feature plugin automaticatlly to prevent any problems caused by conflicts between
* that we deactivate the feature plugin automatically to prevent any problems caused by conflicts between
* the two versions caused by them both being active.
*
* The packages included in this array cannot be deactivated and will always load with WooCommerce core.
*
* @var array Key is the package name/directory, value is the main package class which handles init.
*/
protected static $base_packages = array(
'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package',
'woocommerce-gutenberg-products-block' => '\\Automattic\\WooCommerce\\Blocks\\Package',
);
/**
* Similar to $base_packages, but
* the packages included in this array can be deactivated via the 'woocommerce_merged_packages' filter.
*
* @var array Key is the package name/directory, value is the main package class which handles init.
*/
protected static $merged_packages = array(
'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package',
'woocommerce-gutenberg-products-block' => '\\Automattic\\WooCommerce\\Blocks\\Package',
'woocommerce-brands' => '\\Automattic\\WooCommerce\\Internal\\Brands',
);
/**
* Init the package loader.
*
* @since 3.7.0
*/
public static function init() {
add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ) );
add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ), 0 );
// Prevent plugins already merged into WooCommerce core from getting activated as standalone plugins.
add_action( 'activate_plugin', array( __CLASS__, 'deactivate_merged_plugins' ) );
// Display a notice in the Plugins tab next to plugins already merged into WooCommerce core.
add_filter( 'all_plugins', array( __CLASS__, 'mark_merged_plugins_as_pending_update' ), 10, 1 );
add_action( 'after_plugin_row', array( __CLASS__, 'display_notice_for_merged_plugins' ), 10, 1 );
}
/**
@ -74,6 +94,61 @@ class Packages {
return file_exists( dirname( __DIR__ ) . '/packages/' . $package );
}
/**
* Checks a package exists by looking for it's directory.
*
* @param string $class_name Class name.
* @return boolean
*/
public static function should_load_class( $class_name ) {
foreach ( self::$merged_packages as $merged_package_name => $merged_package_class ) {
if ( str_replace( 'woocommerce-', 'wc_', $merged_package_name ) === $class_name ) {
return true;
}
}
return false;
}
/**
* Gets all merged, enabled packages.
*
* @return array
*/
protected static function get_enabled_packages() {
$enabled_packages = array();
foreach ( self::$merged_packages as $merged_package_name => $package_class ) {
// For gradual rollouts, ensure that a package is enabled for user's remote variant number.
$experimental_package_enabled = method_exists( $package_class, 'is_enabled' ) ?
call_user_func( array( $package_class, 'is_enabled' ) ) :
false;
if ( ! $experimental_package_enabled ) {
continue;
}
$option = 'wc_feature_' . str_replace( '-', '_', $merged_package_name ) . '_enabled';
if ( 'yes' === get_option( $option, 'no' ) ) {
$enabled_packages[ $merged_package_name ] = $package_class;
}
}
return array_merge( $enabled_packages, self::$base_packages );
}
/**
* Checks if a package is enabled.
*
* @param string $package Package name.
* @return boolean
*/
public static function is_package_enabled( $package ) {
return array_key_exists( $package, self::get_enabled_packages() );
}
/**
* Deactivates merged feature plugins.
*
@ -93,7 +168,8 @@ class Packages {
// Deactivate the plugin if possible so that there are no conflicts.
foreach ( $active_plugins as $active_plugin_path ) {
$plugin_file = basename( plugin_basename( $active_plugin_path ), '.php' );
if ( ! isset( self::$merged_packages[ $plugin_file ] ) ) {
if ( ! self::is_package_enabled( $plugin_file ) ) {
continue;
}
@ -118,13 +194,71 @@ class Packages {
}
}
/**
* Prevent plugins already merged into WooCommerce core from getting activated as standalone plugins.
*
* @param string $plugin Plugin name.
*/
public static function deactivate_merged_plugins( $plugin ) {
$plugin_dir = basename( dirname( $plugin ) );
if ( self::is_package_enabled( $plugin_dir ) ) {
$plugins_url = esc_url( admin_url( 'plugins.php' ) );
wp_die(
esc_html__( 'This plugin cannot be activated because its functionality is now included in WooCommerce core.', 'woocommerce' ),
esc_html__( 'Plugin Activation Error', 'woocommerce' ),
array(
'link_url' => esc_url( $plugins_url ),
'link_text' => esc_html__( 'Return to the Plugins page', 'woocommerce' ),
),
);
}
}
/**
* Mark merged plugins as pending update.
* This is required for correctly displaying maintenance notices.
*
* @param array $plugins Plugins list.
*/
public static function mark_merged_plugins_as_pending_update( $plugins ) {
foreach ( $plugins as $plugin_name => $plugin_data ) {
$plugin_dir = basename( dirname( $plugin_name ) );
if ( self::is_package_enabled( $plugin_dir ) ) {
// Necessary to properly display notice within row.
$plugins[ $plugin_name ]['update'] = 1;
}
}
return $plugins;
}
/**
* Displays a maintenance notice next to merged plugins, to inform users
* that the plugin functionality is now offered by WooCommerce core.
*
* Requires 'mark_merged_plugins_as_pending_update' to properly display this notice.
*
* @param string $plugin_file Plugin file.
*/
public static function display_notice_for_merged_plugins( $plugin_file ) {
global $wp_list_table;
$plugin_dir = basename( dirname( $plugin_file ) );
$columns_count = $wp_list_table->get_column_count();
$notice = __( 'This plugin can no longer be activated because its functionality is now included in <strong>WooCommerce</strong>. It is recommended to <strong>delete</strong> it.', 'woocommerce' );
if ( self::is_package_enabled( $plugin_dir ) ) {
echo '<tr class="plugin-update-tr"><td colspan="' . esc_attr( $columns_count ) . '" class="plugin-update"><div class="update-message notice inline notice-error notice-alt"><p>' . wp_kses_post( $notice ) . '</p></div></td></tr>';
}
}
/**
* Loads packages after plugins_loaded hook.
*
* Each package should include an init file which loads the package so it can be used by core.
*/
protected static function initialize_packages() {
foreach ( self::$merged_packages as $package_name => $package_class ) {
foreach ( self::get_enabled_packages() as $package_name => $package_class ) {
call_user_func( array( $package_class, 'init' ) );
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Brand description
*
* This template can be overridden by copying it to yourtheme/woocommerce/brands/brand-description.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 9.4.0
*/
declare( strict_types = 1);
$image_size = wc_get_image_size( 'shop_catalog' ); ?>
<div class="term-description brand-description">
<?php if ( $thumbnail ) : ?>
<img src="<?php echo esc_url( $thumbnail ); ?>" alt="Thumbnail" class="wp-post-image alignright fr brand-thumbnail" width="<?php echo esc_attr( $image_size['width'] ); ?>" />
<?php endif; ?>
<div class="text">
<?php echo do_shortcode( wpautop( wptexturize( term_description() ) ) ); ?>
</div>
</div>

View File

@ -0,0 +1,63 @@
<?php
/**
* Brand A-Z listing
*
* This template can be overridden by copying it to yourtheme/woocommerce/brands/shortcodes/brands-a-z.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @usedby [product_brand_list]
* @version 9.4.0
*/
declare( strict_types = 1);
?>
<div id="brands_a_z">
<ul class="brands_index">
<?php
foreach ( $index as $i ) {
if ( isset( $product_brands[ $i ] ) ) {
echo '<li><a href="#brands-' . esc_attr( $i ) . '">' . esc_html( $i ) . '</a></li>';
} elseif ( $show_empty ) {
echo '<li><span>' . esc_html( $i ) . '</span></li>';
}
}
?>
</ul>
<?php
foreach ( $index as $i ) {
if ( isset( $product_brands[ $i ] ) ) {
?>
<h3 id="brands-<?php echo esc_attr( $i ); ?>"><?php echo esc_html( $i ); ?></h3>
<ul class="brands">
<?php
foreach ( $product_brands[ $i ] as $brand ) {
printf(
'<li><a href="%s">%s</a></li>',
esc_url( get_term_link( $brand->slug, 'product_brand' ) ),
esc_html( $brand->name )
);
}
?>
</ul>
<?php if ( $show_top_links ) { ?>
<a class="top" href="#brands_a_z"><?php esc_html_e( '&uarr; Top', 'woocommerce' ); ?></a>
<?php } ?>
<?php
}
}
?>
</div>

View File

@ -0,0 +1,38 @@
<?php
/**
* Single Brand
*
* This template can be overridden by copying it to yourtheme/woocommerce/brands/shortcodes/single-brand.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
*
* @see WC_Brands::output_product_brand()
*
* @var WP_Term $term The term object.
* @var string $thumbnail The URL to the brand thumbnail.
* @var string $class The class to apply to the thumbnail image.
* @var string $width The width of the image.
* @var string $height The height of the image.
*
* Ignore space indent sniff for this file, as it is used for alignment rather than actual indents.
* phpcs:ignoreFile Generic.WhiteSpace.DisallowSpaceIndent
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @usedby [product_brand]
* @version 9.4.0
*/
declare( strict_types = 1);
?>
<a href="<?php echo esc_url( get_term_link( $term, 'product_brand' ) ); ?>">
<img src="<?php echo esc_url( $thumbnail ); ?>"
alt="<?php echo esc_attr( $term->name ); ?>"
class="<?php echo esc_attr( $class ); ?>"
style="width: <?php echo esc_attr( $width ); ?>; height: <?php echo esc_attr( $height ); ?>;"/>
</a>

View File

@ -0,0 +1,12 @@
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase for backwards compatibility.
/**
* Use the WooCommerce archive template for brand taxonomy pages
*
* @package WooCommerce\Templates
* @version 9.4.0
*/
declare( strict_types = 1);
wc_get_template( 'archive-product.php' );

View File

@ -0,0 +1,27 @@
<?php
/**
* Show a brands description when on a taxonomy page
*
* This template can be overridden by copying it to yourtheme/woocommerce/brands/widgets/brand-description.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 9.4.0
*/
declare( strict_types = 1);
global $woocommerce;
if ( $thumbnail ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wc_get_brand_thumbnail_image( $brand );
}
echo wp_kses_post( wpautop( wptexturize( term_description() ) ) );

View File

@ -0,0 +1,58 @@
<?php
/**
* Show a grid of thumbnails
*
* This template can be overridden by copying it to yourtheme/woocommerce/brands/widgets/brand-thumbnails-description.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 9.4.0
*/
declare( strict_types = 1);
?>
<ul class="brand-thumbnails-description">
<?php
foreach ( $brands as $index => $brand ) :
/**
* Filter the brand's thumbnail size.
*
* @since 9.4.0
* @param string $size Defaults to 'shop_catalog'
*/
$thumbnail = wc_get_brand_thumbnail_url( $brand->term_id, apply_filters( 'woocommerce_brand_thumbnail_size', 'shop_catalog' ) );
if ( ! $thumbnail ) {
$thumbnail = wc_placeholder_img_src();
}
$class = '';
if ( 0 === $index || 0 === $index % $columns ) {
$class = 'first';
} elseif ( 0 === ( $index + 1 ) % $columns ) {
$class = 'last';
}
$width = floor( ( ( 100 - ( ( $columns - 1 ) * 2 ) ) / $columns ) * 100 ) / 100;
?>
<li class="<?php echo esc_attr( $class ); ?>" style="width: <?php echo esc_attr( $width ); ?>%;">
<a href="<?php echo esc_url( get_term_link( $brand->slug, 'product_brand' ) ); ?>" title="<?php echo esc_attr( $brand->name ); ?>" class="term-thumbnail">
<img src="<?php echo esc_url( $thumbnail ); ?>" alt="<?php echo esc_attr( $brand->name ); ?>" />
</a>
<div id="term-<?php echo esc_attr( $brand->term_id ); ?>" class="term-description">
<?php echo wp_kses_post( wpautop( wptexturize( $brand->description ) ) ); ?>
</div>
</li>
<?php endforeach; ?>
</ul>

View File

@ -0,0 +1,45 @@
<?php
/**
* Show a grid of thumbnails
*
* This template can be overridden by copying it to yourtheme/woocommerce/brands/widgets/brand-thumbnails.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 9.4.0
*/
declare( strict_types = 1);
$wrapper_class = 'fluid-columns';
if ( ! $fluid_columns && in_array( $columns, array( 1, 2, 3, 4, 5, 6 ), true ) ) {
$wrapper_class = 'columns-' . $columns;
}
?>
<ul class="brand-thumbnails <?php echo esc_attr( $wrapper_class ); ?>">
<?php
foreach ( array_values( $brands ) as $index => $brand ) :
$class = '';
if ( 0 === $index || 0 === $index % $columns ) {
$class = 'first';
} elseif ( 0 === ( $index + 1 ) % $columns ) {
$class = 'last';
}
?>
<li class="<?php echo esc_attr( $class ); ?>">
<a href="<?php echo esc_url( get_term_link( $brand->slug, 'product_brand' ) ); ?>" title="<?php echo esc_attr( $brand->name ); ?>">
<?php echo wc_get_brand_thumbnail_image( $brand ); // phpcs:ignore WordPress.Security.EscapeOutput ?>
</a>
</li>
<?php endforeach; ?>
</ul>

View File

@ -0,0 +1,42 @@
<!-- wp:template-part {"slug":"header"} /-->
<!-- wp:group {"layout":{"inherit":true,"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:woocommerce/breadcrumbs /-->
<!-- wp:query-title {"type":"archive","showPrefix":false,"align":"wide"} /-->
<!-- wp:term-description {"align":"wide"} /-->
<!-- wp:woocommerce/store-notices /-->
<!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"space-between"}} -->
<div class="wp-block-group alignwide">
<!-- wp:woocommerce/product-results-count /-->
<!-- wp:woocommerce/catalog-sorting /-->
</div>
<!-- /wp:group -->
<!-- wp:query {"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","author":"","search":"","exclude":[],"sticky":"","inherit":true,"__woocommerceAttributes":[],"__woocommerceStockStatus":["instock","outofstock","onbackorder"]},"displayLayout":{"type":"flex","columns":3},"namespace":"woocommerce/product-query","align":"wide"} -->
<div class="wp-block-query alignwide">
<!-- wp:post-template {"className":"products-block-post-template","__woocommerceNamespace":"woocommerce/product-query/product-template"} -->
<!-- wp:woocommerce/product-image {"isDescendentOfQueryLoop":true} /-->
<!-- wp:post-title {"textAlign":"center","level":3,"fontSize":"medium","isLink":true,"__woocommerceNamespace":"woocommerce/product-query/product-title"} /-->
<!-- wp:woocommerce/product-price {"isDescendentOfQueryLoop":true,"textAlign":"center","fontSize":"small","style":{"spacing":{"margin":{"bottom":"1rem"}}}} /-->
<!-- wp:woocommerce/product-button {"isDescendentOfQueryLoop":true,"textAlign":"center","fontSize":"small","style":{"spacing":{"margin":{"bottom":"1rem"}}}} /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination {"layout":{"type":"flex","justifyContent":"center"}} -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
<!-- wp:query-no-results -->
<!-- wp:pattern {"slug":"woocommerce/no-products-found"} /-->
<!-- /wp:query-no-results -->
</div>
<!-- /wp:query -->
</div>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer"} /-->

View File

@ -0,0 +1,5 @@
<!-- wp:template-part {"slug":"header"} /-->
<!-- wp:group {"layout":{"inherit":true}} -->
<div class="wp-block-group"><!-- wp:woocommerce/legacy-template {"template":"archive-product"} /--></div>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer"} /-->

View File

@ -0,0 +1,181 @@
const { test, expect } = require( '@playwright/test' );
test.use( { storageState: process.env.ADMINSTATE } );
test.skip( 'Merchant can add brands', async ( { page } ) => {
/**
* Go to the Brands page.
*
* This will visit the Products page first, and then click on the Brands link.
* This is to workaround the hover menu for now.
*/
const goToBrandsPage = async () => {
await page.goto(
'wp-admin/edit-tags.php?taxonomy=product_brand&post_type=product'
);
// Wait for the Brands page to load.
// This is needed so that checking for existing brands would work.
await page.waitForSelector( '.wp-list-table' );
};
const createBrandIfNotExist = async (
name,
slug,
parentBrand,
description,
thumbnailFileName
) => {
// Create "WooCommerce" brand if it does not exist.
const cellVisible = await page
.locator( '#posts-filter' )
.getByRole( 'cell', { name: slug, exact: true } )
.isVisible();
if ( cellVisible ) {
return;
}
await page.getByRole( 'textbox', { name: 'Name' } ).click();
await page.getByRole( 'textbox', { name: 'Name' } ).fill( name );
await page.getByRole( 'textbox', { name: 'Slug' } ).click();
await page.getByRole( 'textbox', { name: 'Slug' } ).fill( slug );
await page
.getByRole( 'combobox', { name: 'Parent Brand' } )
.selectOption( { label: parentBrand } );
await page.getByRole( 'textbox', { name: 'Description' } ).click();
await page
.getByRole( 'textbox', { name: 'Description' } )
.fill( description );
await page.getByRole( 'button', { name: 'Upload/Add image' } ).click();
await page.getByRole( 'tab', { name: 'Media Library' } ).click();
await page.getByRole( 'checkbox', { name: thumbnailFileName } ).click();
await page.getByRole( 'button', { name: 'Use image' } ).click();
await page.getByRole( 'button', { name: 'Add New Brand' } ).click();
// We should see an "Item added." notice message at the top of the page.
await expect(
page.locator( '#ajax-response' ).getByText( 'Item added.' )
).toBeVisible();
// We should see the newly created brand in the Brands table.
await expect(
page
.locator( '#posts-filter' )
.getByRole( 'cell', { name: slug, exact: true } )
).toHaveCount( 1 );
};
/**
* Edit a brand.
*
* You must be in the Brands page before calling this function.
* To do so, call `goToBrandsPage()` first.
*
* After a brand is edited, you will be redirected to the Brands page.
*/
const editBrand = async (
currentName,
{ name, slug, parentBrand, description, thumbnailFileName }
) => {
await page.getByLabel( `${ currentName }” (Edit)` ).click();
await page.getByLabel( 'Name' ).fill( name );
await page.getByLabel( 'Slug' ).fill( slug );
await page
.getByLabel( 'Parent Brand' )
.selectOption( { label: parentBrand } );
await page.getByLabel( 'Description' ).fill( description );
await page.getByRole( 'button', { name: 'Upload/Add image' } ).click();
await page.getByRole( 'tab', { name: 'Media Library' } ).click();
await page.getByLabel( thumbnailFileName ).click();
await page.getByRole( 'button', { name: 'Use image' } ).click();
await page.getByRole( 'button', { name: 'Update' } ).click();
// We should see an "Item updated." notice message at the top of the page.
await expect(
page.locator( '#message' ).getByText( 'Item updated.' )
).toBeVisible();
// navigate back to Brands page.
await page.getByRole( 'link', { name: '← Go to Brands' } ).click();
// confirm that the brand has been updated.
await expect(
page
.locator( '#posts-filter' )
.getByRole( 'cell', { name: slug, exact: true } )
).toHaveCount( 1 );
};
/**
* Delete a brand.
*
* You must be in the Brands page before calling this function.
* To do so, call `goToBrandsPage()` first.
*
* After a brand is deleted, you will be redirected to the Brands page.
*/
const deleteBrand = async ( name ) => {
await page.getByLabel( `${ name }” (Edit)` ).click();
// After clicking the "Delete" button, there will be a confirmation dialog.
page.once( 'dialog', ( dialog ) => {
// Click "OK" to confirm the deletion.
dialog.accept();
} );
// Click on the "Delete" button.
await page.getByRole( 'link', { name: 'Delete' } ).click();
// We should now be in the Brands page.
// Confirm that the brand has been deleted and is no longer in the Brands table.
await expect(
page
.locator( '#posts-filter' )
.getByRole( 'cell', { name, exact: true } )
).toHaveCount( 0 );
};
await goToBrandsPage();
await createBrandIfNotExist(
'WooCommerce',
'woocommerce',
'None',
'All things WooCommerce!',
'image-01'
);
// Create child brand under the "WooCommerce" parent brand.
await createBrandIfNotExist(
'WooCommerce Apparels',
'woocommerce-apparels',
'WooCommerce',
'Cool WooCommerce clothings!',
'image-02'
);
// Create a dummy child brand called "WooCommerce Dummy" under the "WooCommerce" parent brand.
await createBrandIfNotExist(
'WooCommerce Dummy',
'woocommerce-dummy',
'WooCommerce',
'Dummy WooCommerce brand!',
'image-02'
);
// Edit the dummy child brand from "WooCommerce Dummy" to "WooCommerce Dummy Edited".
await editBrand( 'WooCommerce Dummy', {
name: 'WooCommerce Dummy Edited',
slug: 'woocommerce-dummy-edited',
parentBrand: 'WooCommerce',
description: 'Dummy WooCommerce brand edited!',
thumbnailFileName: 'image-03',
} );
// Delete the dummy child brand "WooCommerce Dummy Edited".
await deleteBrand( 'WooCommerce Dummy Edited' );
} );

View File

@ -37,6 +37,12 @@ const couponData = {
amount: '60',
excludeProductCategories: [ 'Uncategorized' ],
},
excludeProductBrands: {
code: `excludeProductBrands-${ new Date().getTime().toString() }`,
description: 'Exclude product brands coupon',
amount: '65',
excludeProductBrands: [ 'WooCommerce Apparels' ],
},
products: {
code: `products-${ new Date().getTime().toString() }`,
description: 'Products coupon',
@ -202,6 +208,26 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
.click();
} );
}
// Skip Brands tests while behind a feature flag.
const skipBrandsTests = true;
// set exclude product brands
if ( couponType === 'excludeProductBrands' && ! skipBrandsTests ) {
await test.step( 'set exclude product brands coupon', async () => {
await page
.getByRole( 'link', {
name: 'Usage restriction',
} )
.click();
await page
.getByPlaceholder( 'No brands' )
.pressSequentially( 'WooCommerce Apparels' );
await page
.getByRole( 'option', { name: 'WooCommerce Apparels' } )
.click();
} );
}
// set products
if ( couponType === 'products' ) {
await test.step( 'set products coupon', async () => {

View File

@ -0,0 +1,116 @@
<?php
/**
* WooCommerce Brands Unit tests suit
*
* @package woocommerce-brands
*/
declare( strict_types = 1);
require_once WC_ABSPATH . '/includes/admin/class-wc-admin-brands.php';
require_once WC_ABSPATH . '/includes/class-wc-brands.php';
/**
* WC Brands Admin test
*/
class WC_Admin_Brands_Test extends WC_Unit_Test_Case {
/**
* Tests brands filter outputs as a standard dropdown.
*
* @return void
*/
public function test_product_brand_filter_render_outputs_a_dropdown() {
$simple_product = WC_Helper_Product::create_simple_product();
WC_Brands::init_taxonomy();
$term_a_id = $this->factory()->term->create(
array(
'taxonomy' => 'product_brand',
'name' => 'Blah_A',
)
);
$term_b_id = $this->factory()->term->create(
array(
'taxonomy' => 'product_brand',
'name' => 'Foo_A',
)
);
$term_c_id = $this->factory()->term->create(
array(
'taxonomy' => 'product_brand',
'name' => 'Blah_B',
)
);
wp_set_post_terms( $simple_product->get_id(), array( $term_a_id, $term_b_id, $term_c_id ), 'product_brand' );
add_filter(
'woocommerce_product_brand_filter_threshold',
function () {
return 3;
}
);
$brands_admin = new WC_Brands_Admin();
ob_start();
$brands_admin->render_product_brand_filter();
$output = ob_get_contents();
ob_end_clean();
$this->assertStringContainsString(
'<select name=\'product_brand\' id=\'product_brand\' class=\'dropdown_product_brand\'',
$output
);
}
/**
* Tests brands filter outputs as a custom search-select component.
*
* @return void
*/
public function test_product_brand_filter_render_outputs_a_select() {
$simple_product = WC_Helper_Product::create_simple_product();
WC_Brands::init_taxonomy();
$term_a_id = $this->factory()->term->create(
array(
'taxonomy' => 'product_brand',
'name' => 'Blah_A',
)
);
$term_b_id = $this->factory()->term->create(
array(
'taxonomy' => 'product_brand',
'name' => 'Foo_A',
)
);
$term_c_id = $this->factory()->term->create(
array(
'taxonomy' => 'product_brand',
'name' => 'Blah_B',
)
);
wp_set_post_terms( $simple_product->get_id(), array( $term_a_id, $term_b_id, $term_c_id ), 'product_brand' );
add_filter(
'woocommerce_product_brand_filter_threshold',
function () {
return 2;
}
);
$brands_admin = new WC_Brands_Admin();
ob_start();
$brands_admin->render_product_brand_filter();
$output = ob_get_contents();
ob_end_clean();
$this->assertStringContainsString(
'<select class="wc-brands-search" name="product_brand" data-placeholder="Filter by brand" data-allow_clear="true"',
$output
);
}
}