Merge branch 'trunk' into update/rename-edit-fn-names

This commit is contained in:
Damián Suárez 2023-11-30 08:15:26 -03:00
commit ac18ae7db4
131 changed files with 4429 additions and 1731 deletions

View File

@ -32,7 +32,7 @@ jobs:
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
run: echo "date=$(date +'%Y-%m-%d-%H-%M')" >> $GITHUB_OUTPUT
- name: Set all package string
id: all_description

View File

@ -1,4 +1,4 @@
# WooCommerce Developer Documentation
# WooCommerce developer documentation
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!

View File

@ -1,4 +1,4 @@
# Rename a country
# Change a currency symbol
See the [currency list](https://woocommerce.github.io/code-reference/files/woocommerce-includes-wc-core-functions.html#source-view.475) for reference on currency codes.

View File

@ -1,4 +1,4 @@
# Useful Core Functions
# Useful core functions
WooCommerce core functions are available on both front-end and admin. They can be found in `includes/wc-core-functions.php` and can be used by themes in plugins.

View File

@ -1,4 +1,4 @@
# CSS SASS coding guidelines and naming convetions
# CSS SASS coding guidelines and naming conventions
Our guidelines are based on those used in [Calypso](https://github.com/Automattic/wp-calypso) which itself follows the BEM methodology. Refer to [this doc](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md?term=css) for full details. There are a few differences in WooCommerce however which are outlined below;

View File

@ -1,4 +1,4 @@
# API Critical Flows
# API critical flows
In our documentation, we've pinpointed the essential user flows within the WooCommerce Core API. These flows serve as
the compass for our testing initiatives, aiding us in concentrating our efforts where they matter most. They also

View File

@ -1,4 +1,4 @@
# How to decide if a Pull Request is High-Impact
# How to decide if a pull request is high impact
Deciding if a Pull Request should be declared High-Impact is a complex task. To achieve it, we need to assess and estimate the impact that the changes introduced in the Pull Request have in WooCommerce, which is usually a subjective task and sometimes inaccurate, due to the huge knowledge it demands of the WooCommerce product details, technical details and even customers issues history.

View File

@ -1,4 +1,4 @@
# Deprecation in Core
# Deprecation in core
Deprecation is a method of discouraging usage of a feature or practice in favour of something else without breaking backwards compatibility or totally prohibiting its usage. To quote the Wikipedia article on Deprecation:

View File

@ -1,4 +1,4 @@
# Naming Conventions
# Naming conventions
## PHP

View File

@ -1,4 +1,4 @@
# WooCommerce Git Flow
# WooCommerce Git flow
For core development, we use the following structure and flow.

View File

@ -1,4 +1,4 @@
# Data Stores
# Data stores
## Introduction

View File

@ -1,5 +1,5 @@
# Adding a Section to a Settings Tab
# Adding a section to a settings tab
When youre adding building an extension for WooCommerce that requires settings of some kind, its important to ask yourself: **Where do they belong?** If your extension just has a couple of simple settings, do you really need to create a new tab specifically for it? Most likely the answer is no.

View File

@ -1,4 +1,4 @@
# WooCommerce Extension Developer Handbook
# WooCommerce extension developer handbook
Want to create a plugin to extend WooCommerce? WooCommerce extensions are the same as regular WordPress plugins. For more information, visit [Writing a plugin](https://developer.wordpress.org/plugins/).

View File

@ -0,0 +1,74 @@
# GDPR Compliance Guidelines for WooCommerce Extensions
## Introduction
The General Data Protection Regulation (GDPR) is in effect, granting EU residents increased rights over their personal data. Developers must ensure that WooCommerce extensions are compliant with these regulations.
## Data Sharing and Collection
### Third-Party Data Sharing
- Assess and document any third-party data sharing.
- Obtain and manage user consent for data sharing.
- Link to third-party privacy policies in your plugin settings.
### Data Collection
- List the personal data your plugin collects.
- Secure consent for data collection and manage user preferences.
- Safeguard data storage and restrict access to authorized personnel.
## Data Access and Storage
### Accessing Personal Data
- Specify what personal data your plugin accesses from WooCommerce orders.
- Justify the necessity for accessing each type of data.
- Control access to personal data based on user roles and permissions.
### Storing Personal Data
- Explain your data storage mechanisms and locations.
- Apply encryption to protect stored personal data.
- Perform regular security audits.
## Personal Data Handling
### Data Exporter and Erasure Hooks
- Integrate data exporter and erasure hooks to comply with user requests.
- Create a user-friendly interface for data management requests.
### Refusal of Data Erasure
- Define clear protocols for instances where data erasure is refused.
- Communicate these protocols transparently to users.
## Frontend and Backend Data Exposure
### Data on the Frontend
- Minimize personal data displayed on the site's frontend.
- Provide configurable settings for data visibility based on user status.
### Data in REST API Endpoints
- Ensure REST API endpoints are secure and disclose personal data only as necessary.
- Establish clear permissions for accessing personal data via the API.
## Privacy Documentation and Data Management
### Privacy Policy Documentation
- Maintain an up-to-date privacy policy detailing your plugins data handling.
- Include browser storage methods and third-party data sharing in your documentation.
### Data Cleanup
- Implement data cleanup protocols for plugin uninstallation and deletion of orders/users.
- Automate personal data removal processes where appropriate.
## Conclusion
- Keep a record of GDPR compliance measures and make them accessible to users.
- Update your privacy policy regularly to align with any changes in data processing activities.

View File

@ -1,4 +1,4 @@
# Implementing Settings for Extensions
# Implementing settings for extensions
If youre customizing WooCommerce or adding your own functionality to it youll probably need a settings page of some sort. One of the easiest ways to create a settings page is by taking advantage of the [`WC_Integration` class](https://woocommerce.github.io/code-reference/classes/WC-Integration.html 'WC_Integration Class'). Using the Integration class will automatically create a new settings page under **WooCommerce > Settings > Integrations** and it will automatically save, and sanitize your data for you. Weve created this tutorial so you can see how to create a new integration.

View File

@ -1,4 +1,4 @@
# Extension Development
# Extension development
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!

View File

@ -1,4 +1,4 @@
# WooCommerce Developer Resources
# WooCommerce developer resources
This guide is a great starting point for WooCommerce development. From setting up your first online store to diving deep into advanced features, you'll find what you need here. New to WooCommerce? Start with the basics. Experienced and looking for specific documentation or community discussions? We've got that covered too. Navigate through the sections below to find the resources tailored for you.

View File

@ -1,4 +1,4 @@
# WooCommerce Developer Tools
# WooCommerce developer tools
This guide provides an overview of essential tools and libraries for WooCommerce development. It's intended for developers looking to enhance their WooCommerce projects efficiently.

View File

@ -1,4 +1,4 @@
# Getting-started
# Getting started
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!

View File

@ -0,0 +1,120 @@
# Set up and use a child theme
**Note:** This document is intended for creating and using classic child themes. For a comprehensive guide on creating a child block theme and understanding the differences between a classic and block theme, please refer to [this detailed documentation](https://learn.wordpress.org/lesson-plan/create-a-basic-child-theme-for-block-themes/).
Sometimes, you might need to customize your theme or WooCommerce beyond what is possible via the options. These guidelines will teach you the basics of how to go about customizing your site by using a child theme.
## What is a child theme?
Before we start its important that you understand what a child theme is. In short, a child theme is a layer that you put on top of the parent theme to make alterations without having to develop a new theme from scratch. There are two major reasons to use child themes:
- Theme developers can use child themes as a way to offer variations on a theme, similar to what we do with the [Storefront child themes](https://woo.com/products/storefront/)
- Developers can use child themes to host customizations of the parent theme or any plugin on the site since the child theme will get priority over the plugins and parent theme
Read [this guide from the WordPress Codex](https://developer.wordpress.org/themes/advanced-topics/child-themes/).
## Make a backup
Before customizing a website, you should always ensure that you have a backup of your site in case anything goes wrong. More info at: [Backing up WordPress content](https://woo.com/document/backup-wordpress-content/).
## Getting started
To get started, we need to prepare a child theme.
### Making the child theme
First, we need to create a new stylesheet for our child theme. Create a new file called `style.css` and put this code in it:
```css
/*
Theme Name: Child Theme
Version: 1.0
Description: Child theme for Woo.
Author: Woo
Author URI: https://woo.com
Template: themedir
*/
```
Next, we need to change the **Template** field to point to our installed WooTheme. In this example, well use the Storefront theme, which is installed under `wp-content/themes/storefront/`. The result will look like this:
```css
/*
Theme Name: Storefront Child
Version: 1.0
Description: Child theme for Storefront.
Author: Woo
Author URI: https://woo.com
Template: storefront
*/
/* --------------- Theme customization starts here ----------------- */
```
**Note:** With Storefront, you do not need to enqueue any of the parent theme style files with PHP from the themes `functions.php` file or `@import` these into the child themes `style.css` file as the main parent Storefront theme does this for you.
With Storefront, a child theme only requires a blank `functions.php` file and a `style.css` file to get up and running.
## Uploading and activating
You can upload the child theme either through your FTP client, or using the Add New theme option in WordPress.
- **Through FTP.** If youre using FTP, it means that you go directly to the folders of your website. That means youll need **FTP access** to your host, so you can upload the new child theme. If you dont have this, you should talk to your host and they can give you your FTP login details, and then download an FTP program to upload your files.
- **Through the WP Dashboard.** If you create a .zip file of your child theme folder you can then simply upload that to your site from the **WordPress > Appearance > Themes > Add New** section.
Once youve done that, your child theme will be uploaded to a new folder in `wp-content/themes/`, for example, `wp-content/themes/storefront-child/`. Once uploaded, we can go to our **WP Dashboard > Appearance > Themes** and activate the child theme.
## Customizing design and functionality
Your child theme is now ready to be modified. Currently, it doesnt hold any customization, so lets look at a couple of examples of how we can customize the child theme without touching the parent theme.
### Design customization
Lets do an example together where we change the color of the site title. Add this to your `/storefront-child/style.css`:
```css
.site-branding h1 a {
color: red;
}
```
After saving the file and refreshing our browser, you will now see that the color of the site title has changed!
### Template changes
**Note:** This doesnt apply to Storefront child themes. Any customizations to a Storefront child themes files will be lost when updating. Instead of customizing the Storefront child themes files directly, we recommended that you add code snippets to a customization plugin. Weve created one to do just this. Download [Theme Customizations](https://github.com/woocommerce/theme-customisations) for free.
But wait, theres more! You can do the same with the template files (`*.php`) in the theme folder. For example if w, wanted to modify some code in the header, we need to copy header.php from our parent theme folder `wp-content/themes/storefront/header.php` to our child theme folder `wp-content/themes/storefront-child/header.php`. Once we have copied it to our child theme, we edit `header.php` and customize any code we want. The `header.php` in the child theme will be used instead of the parent themes `header.php`.
The same goes for WooCommerce templates. If you create a new folder in your child theme called “WooCommerce”, you can make changes to the WooCommerce templates there to make it more in line with the overall design of your website. More on WooCommerces template structure [can be found here](https://woo.com/document/template-structure/).
### Functionality changes
**NOTE**: The functions.php in your child theme should be **empty** and not include anything from the parent themes functions.php.
The `functions.php` in your child theme is loaded **before** the parent themes `functions.php`. If a function in the parent theme is **pluggable**, it allows you to copy a function from the parent theme into the child themes `functions.php` and have it replace the one in your parent theme. The only requirement is that the parent themes function is **pluggable**, which basically means it is wrapped in a conditional if statement e.g:
```php
if ( ! function_exists( "parent_function_name" ) ) {
parent_function_name() {
...
}
}
```
If the parent theme function is **pluggable**, you can copy it to the child theme `functions.php` and modify the function to your liking.
## Template directory vs stylesheet directory
WordPress has a few things that it handles differently in child themes. If you have a template file in your child theme, you have to modify how WordPress includes files. `get_template_directory()` will reference the parent theme. To make it use the file in the child theme, you need to change use `get_stylesheet_directory();`.
[More info on this from the WP Codex](https://developer.wordpress.org/themes/advanced-topics/child-themes/#referencing-or-including-other-files)
## Child theme support
Although we do offer basic child theme support that can easily be answered, it still falls under theme customization, so please refer to our [support policy](https://woo.com/support-policy/) to see the extent of support we give. We highly advise anybody confused with child themes to use the [WordPress forums](https://wordpress.org/support/forums/) for help.
## Sample child theme
Download the sample child theme at the top of this article to get started. Place the child theme in your **wp-content/themes/** folder along with your parent theme.

View File

@ -1,4 +1,4 @@
# Payment Gateway API
# Payment gateway API
Payment gateways in WooCommerce are class based and can be added through traditional plugins. This guide provides an intro to gateway development.

View File

@ -1,4 +1,4 @@
# Payment Token API
# Payment token API
WooCommerce 2.6 introduced an API for storing and managing payment tokens for gateways. Users can also manage these tokens from their account settings and choose from saved payment tokens on checkout.

View File

@ -1,4 +1,4 @@
# Product Editor Extensibility Guidelines
# Product editor extensibility guidelines
> ⚠️ **Notice:** These guidelines are currently a **work in progress**. Please be aware that some details might be incomplete or subject to change. We appreciate your patience and welcome any contributions!

View File

@ -1,4 +1,4 @@
# CSS/Sass Naming Conventions
# CSS/Sass naming conventions
Table of Contents:

View File

@ -1,4 +1,4 @@
# Naming Conventions
# Naming conventions
Table of Contents:

View File

@ -1,4 +1,4 @@
# Quality and Best Practices
# Quality and best practices
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!

View File

@ -1,5 +0,0 @@
# Reference Code
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Dive deep into code snippets, examples, and templates tailored for WooCommerce. This section will serve as a valuable resource for developers, providing reusable pieces of code that can be integrated into various WooCommerce projects.

View File

@ -1,4 +1,4 @@
# Extending WC-Admin reports
# Extending WooCommerce Analytics reports
## Introduction

View File

@ -1,4 +1,4 @@
# Shipping Method API
# Shipping method API
WooCommerce has a shipping method API which plugins can use to add their own rates. This article will take you through the steps to creating a new shipping method and interacting with the API.

View File

@ -0,0 +1,221 @@
# Classic Theme Developer Handbook
---
**Note:** this document is geared toward the development of classic themes. For the recommended modern approach, visit [Develop Your First Low-Code Block Theme](https://learn.wordpress.org/course/develop-your-first-low-code-block-theme/) to learn about block theme development, and explore the [Create Block Theme plugin](https://wordpress.org/plugins/create-block-theme/) tool when you're ready to create a new theme.
---
WooCommerce looks great with all WordPress themes as of version 3.3, even if they are not WooCommerce-specific themes and do not formally declare support. Templates render inside the content, and this keeps everything looking natural on your site.
Non-WooCommerce themes, by default, also include:
- Zoom feature enabled ability to zoom in/out on a product image
- Lightbox feature enabled product gallery images pop up to examine closer
- Comments enabled, not Reviews visitors/buyers can leave comments as opposed to product ratings or reviews
If you want more control over the layout of WooCommerce elements or full reviews support your theme will need to integrate with WooCommerce. There are a few different ways you can do this, and they are outlined below.
## Theme Integration
There are three possible ways to integrate WooCommerce with a theme. If you are using WooCommerce 3.2 or below (**strongly discouraged**) you will need to use one of these methods to ensure WooCommerce shop and product pages are rendered correctly in your theme. If you are using a version of WooCommerce 3.3 or above you only need to do a theme integration if the automatic one doesnt meet your needs.
### Using `woocommerce_content()`
This solution allows you to create a new template page within your theme that is used for **all WooCommerce taxonomy and post type displays**. While an easy catch-all solution, it does have a drawback in that this template is used for **all WooCommerce taxonomies** (product categories, etc.) and **post types** (product archives, single product pages). Developers are encouraged to use the hooks instead (see below).
To set up this template page:
1. **Duplicate page.php:** Duplicate your themes `page.php` file, and name it `woocommerce.php`. This path to the file should follow this pattern: `wp-content/themes/YOURTHEME/woocommerce.php`.
2. **Edit your page (woocommerce.php)**: Open up your newly created `woocommerce.php` in a text editor.
3. **Replace the loop:** Next you need to find the loop (see [The_Loop](https://codex.wordpress.org/The_Loop)). The loop usually starts with code like this:
```php
<?php if ( have_posts() ) :
```
It usually ends with this:
```php
<?php endif; ?>
```
This varies between themes. Once you have found it, **delete it**. In its place, put:
```php
<?php woocommerce_content(); ?>
```
This will make it use **WooCommerces loop instead**. Save the file. Youre done.
**Note:** When creating `woocommerce.php` in your themes folder, you will not be able to override the `woocommerce/archive-product.php` custom template as `woocommerce.php` has priority over `archive-product.php`. This is intended to prevent display issues.
### Using hooks
The hook method is more involved, but it is also more flexible. This is similar to the method we use when creating themes. Its also the method we use to integrate nicely with WordPress default themes.
Insert a few lines in your themes `functions.php` file.
First unhook the WooCommerce wrappers:
```php
remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10);
remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10);
```
Then hook in your own functions to display the wrappers your theme requires:
```php
add_action('woocommerce_before_main_content', 'my_theme_wrapper_start', 10);
add_action('woocommerce_after_main_content', 'my_theme_wrapper_end', 10);
function my_theme_wrapper_start() {
echo '<section id="main">';
}
function my_theme_wrapper_end() {
echo '</section>';
}
```
Make sure that the markup matches that of your theme. If youre unsure of which classes or IDs to use, take a look at your themes `page.php` for guidance.
**Whenever possible use the hooks to add or remove content. This method is more robust than overriding the templates.** If you have overridden a template, you have to update the template any time the file changes. If you are using the hooks, you will only have to update if the hooks change, which happens much less frequently.
### Using template overrides
For information about overriding the WooCommerce templates with your own custom templates read the **Template Structure** section below. This method requires more maintenance than the hook-based method, as templates will need to be kept up-to-date with the WooCommerce core templates.
## Declaring WooCommerce Support
If you are using custom WooCommerce template overrides in your theme you need to declare WooCommerce support using the `add_theme_support` function. WooCommerce template overrides are only enabled on themes that declare WooCommerce support. If you do not declare WooCommerce support in your theme, WooCommerce will assume the theme is not designed for WooCommerce compatibility and will use shortcode-based unsupported theme rendering to display the shop.
Declaring WooCommerce support is straightforward and involves adding one function in your themes `functions.php` file.
### Basic Usage
```php
function mytheme_add_woocommerce_support() {
add_theme_support( 'woocommerce' );
}
add_action( 'after_setup_theme', 'mytheme_add_woocommerce_support' );
```
Make sure you are using the `after_setup_theme` hook and not the `init` hook. Read more about this in [the documentation for `add_theme_support`](https://developer.wordpress.org/reference/functions/add_theme_support/).
### Usage with Settings
```php
function mytheme_add_woocommerce_support() {
add_theme_support( 'woocommerce', array(
'thumbnail_image_width' => 150,
'single_image_width' => 300,
'product_grid' => array(
'default_rows' => 3,
'min_rows' => 2,
'max_rows' => 8,
'default_columns' => 4,
'min_columns' => 2,
'max_columns' => 5,
),
) );
}
add_action( 'after_setup_theme', 'mytheme_add_woocommerce_support' );
```
These are optional theme settings that you can set when declaring WooCommerce support.
`thumbnail_image_width` and `single_image_width` will set the image sizes for the shop. If these are not declared when adding theme support, the user can set image sizes in the Customizer under the **WooCommerce > Product Images** section.
The `product_grid` settings let theme developers set default, minimum, and maximum column and row settings for the Shop. Users can set the rows and columns in the Customizer under the **WooCommerce > Product Catalog** section.
### Product gallery features (zoom, swipe, lightbox)
The product gallery introduced in 3.0.0 ([read here for more information](https://developer.woo.com/2016/10/19/new-product-gallery-merged-in-to-core-for-2-7/)) uses Flexslider, Photoswipe, and the jQuery Zoom plugin to offer swiping, lightboxes, and other neat features.
In versions `3.0`, `3.1`, and `3.2`, the new gallery is off by default and needs to be enabled using a snippet (below) or by using a compatible theme. This is because its common for themes to disable the WooCommerce gallery and replace it with their own scripts.
In versions `3.3+`, the gallery is off by default for WooCommerce compatible themes unless they declare support for it (below). 3rd party themes with no WooCommerce support will have the gallery enabled by default.
To enable the gallery in your theme, you can declare support like this:
```php
add_theme_support( 'wc-product-gallery-zoom' );
add_theme_support( 'wc-product-gallery-lightbox' );
add_theme_support( 'wc-product-gallery-slider' );
```
You do not have to support all three parts of the gallery; you can pick and choose features. If a feature is not enabled, the scripts will not be loaded and the gallery code will not execute on product pages.
If gallery features are enabled (e.g., you have a theme that enabled them, or you are running a theme that is not compatible with WooCommerce), you can disable them with `remove_theme_support`:
```php
remove_theme_support( 'wc-product-gallery-zoom' );
remove_theme_support( 'wc-product-gallery-lightbox' );
remove_theme_support( 'wc-product-gallery-slider' );
```
You can disable any parts; you do not need to disable all features.
## Template Structure
WooCommerce template files contain the **markup** and **template structure** for **the frontend and the HTML emails** of your store. If some structural change in HTML is necessary, you should override a template.
When you open these files, you will notice they all contain **hooks** that allow you to add or move content without needing to edit the template files themselves. This method protects against upgrade issues, as the template files can be left completely untouched.
Template files can be found within the `**/woocommerce/templates/**` directory.
### How to Edit Files
Edit files in an **upgrade-safe way** using *overrides*. Copy them into a directory within your theme named `/woocommerce`, keeping the same file structure but removing the `/templates/` subdirectory.
Example: To override the admin order notification, copy `wp-content/plugins/woocommerce/templates/emails/admin-new-order.php` to `wp-content/themes/yourtheme/woocommerce/emails/admin-new-order.php`.
The copied file will now override the WooCommerce default template file.
**Warning:** Do not delete any WooCommerce hooks when overriding a template. This would prevent plugins hooking in to add content.
**Warning:** Do not edit these files within the core plugin itselfe as they are overwritten during the upgrade process and any customizations will be lost.
## CSS Structure
Inside the `assets/css/` directory, you will find the stylesheets responsible for the default WooCommerce layout styles.
Files to look for are `woocommerce.scss` and `woocommerce.css`.
- `woocommerce.css` is the minified stylesheet its the CSS without any of the spaces, indents, etc. This makes the file very fast to load. This file is referenced by the plugin and declares all WooCommerce styles.
- `woocommerce.scss` is not directly used by the plugin, but by the team developing WooCommerce. We use [SASS](http://sass-lang.com/) in this file to generate the CSS in the first file.
The CSS is written to make the default layout compatible with as many themes as possible by using percentage-based widths for all layout styles. It is, however, likely that youll want to make your own adjustments.
### Modifications
To avoid upgrade issues, we advise not editing these files but rather using them as a point of reference.
If you just want to make changes, we recommend adding some overriding styles to your theme stylesheet. For example, add the following to your theme stylesheet to make WooCommerce buttons black instead of the default color:
```css
a.button,
button.button,
input.button,
#review_form #submit {
background:black;
}
```
WooCommerce also outputs the theme name (plus other useful information, such as which type of page is being viewed) as a class on the body tag, which can be useful for overriding styles.
### Disabling WooCommerce styles
If you plan to make major changes, or create a theme from scratch, then you may prefer your theme not reference the WooCommerce stylesheet at all. You can tell WooCommerce to not use the default `woocommerce.css` by adding the following code to your themes `functions.php` file:
```php
add_filter( 'woocommerce_enqueue_styles', '__return_false' );
```
With this definition in place, your theme will no longer use the WooCommerce stylesheet and give you a blank canvas upon which you can build your own desired layout and styles.
Styling a WooCommerce theme from scratch for the first time is no easy task. There are many different pages and elements that need to be styled, and if youre new to WooCommerce, you are probably not familiar with many of them. A non-exhaustive list of WooCommerce elements to style can be found [here](https://developer.files.wordpress.com/2017/12/woocommerce-theme-testing-checklist.pdf).

View File

@ -0,0 +1,96 @@
# Conditional tags
**Note:** This is a **Developer level** doc. If you are unfamiliar with code/tags and resolving potential conflicts, select a [WooExpert or Developer](https://woo.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](https://woo.com/support-policy/).
## What are “conditional tags”?
The conditional tags of WooCommerce and WordPress can be used in your template files to change what content is displayed based on what *conditions* the page matches. For example, you may want to display a snippet of text above the shop page. With the `is_shop()` conditional tag, you can.
Because WooCommerce uses custom post types, you can also use many of WordPress conditional tags. See [codex.wordpress.org/Conditional_Tags](https://codex.wordpress.org/Conditional_Tags) for a list of the tags included with WordPress.
**Note**: You can only use conditional query tags after the `posts_selection` [action hook](https://codex.wordpress.org/Plugin_API/Action_Reference#Actions_Run_During_a_Typical_Request) in WordPress (the `wp` action hook is the first one through which you can use these conditionals). For themes, this means the conditional tag will never work properly if you are using it in the body of functions.php.
## Available conditional tags
All conditional tags test whether a condition is met, and then return either `TRUE` or `FALSE`. **Conditions under which tags output `TRUE` are listed below the conditional tags**.
The list below holds the main conditional tags. To see all conditional tags, visit the [WooCommerce API Docs](https://woo.com/wc-apidocs/).
### WooCommerce page
- `is_woocommerce()`
Returns true if on a page which uses WooCommerce templates (cart and checkout are standard pages with shortcodes and thus are not included).
### Main shop page
- `is_shop()`
Returns true when on the product archive page (shop).
### Product category page
- `is_product_category()`
Returns true when viewing a product category archive.
- `is_product_category( 'shirts' )`
When the product category page for the shirts category is being displayed.
- `is_product_category( array( 'shirts', 'games' ) )`
When the product category page for the shirts or games category is being displayed.
### Product tag page
- `is_product_tag()`
Returns true when viewing a product tag archive
- `is_product_tag( 'shirts' )`
When the product tag page for the shirts tag is being displayed.
- `is_product_tag( array( 'shirts', 'games' ) )`
When the product tag page for the shirts or games tags is being displayed.
### Single product page
- `is_product()`
Returns true on a single product page. Wrapper for is_singular.
### Cart page
- `is_cart()`
Returns true on the cart page.
### Checkout page
- `is_checkout()`
Returns true on the checkout page.
### Customer account pages
- `is_account_page()`
Returns true on the customers account pages.
### Endpoint
- `is_wc_endpoint_url()`
Returns true when viewing a WooCommerce endpoint
- `is_wc_endpoint_url( 'order-pay' )`
When the endpoint page for order pay is being displayed.
- And so on for other endpoints...
### Ajax request
- `is_ajax()`
Returns true when the page is loaded via ajax.
## Working example
The example illustrates how you would display different content for different categories.
```php
if ( is_product_category() ) {
if ( is_product_category( 'shirts' ) ) {
echo 'Hi! Take a look at our sweet t-shirts below.';
} elseif ( is_product_category( 'games' ) ) {
echo 'Hi! Hungry for some gaming?';
} else {
echo 'Hi! Check out our products below.';
}
}
```

View File

@ -1,4 +1,4 @@
# Fixing Outdated WooCommerce Templates
# Fixing outdated WooCommerce templates
## Template Updates and Changes

View File

@ -1,4 +1,4 @@
# Theme Design and User Experience Guidelines
# Theme design and user experience guidelines
This guide covers general guidelines and best practices to follow in order to ensure your theme experience aligns with ecommerce industry standards and WooCommerce for providing a great online shopping experience, maximizing sales, ensuring ease of use, seamless integration, and strong UX adoption.

View File

@ -1,4 +1,4 @@
# User Experience Guidelines: Accessibility
# User experience guidelines: accessibility
## Accessibility

View File

@ -1,4 +1,4 @@
# User Experience Guidelines: Best Practices
# User experience guidelines: best practices
## Best practices

View File

@ -1,4 +1,4 @@
# User Experience Guidelines: Colors
# User experience guidelines: colors
## Colors

View File

@ -1,4 +1,4 @@
# User Experience Guidelines: Notices
# User experience guidelines: notices
## Notices

View File

@ -1,4 +1,4 @@
# User Experience Guidelines: Onboarding
# User experience guidelines: onboarding
## Onboarding

View File

@ -1,4 +1,4 @@
# User Experience Guidelines: Task list and Inbox
# User experience guidelines: task list and inbox
## Task List & Inbox

View File

@ -1,4 +1,4 @@
# User Experience Guidelines
# User experience guidelines
This guide covers general guidelines, and best practices to follow in order to ensure your product experience aligns with WooCommerce for ease of use, seamless integration, and strong adoption.

View File

@ -0,0 +1,100 @@
# Configuring Caching Plugins for WooCommerce
## Excluding Pages from the Cache
Oftentimes if using caching plugins theyll already exclude these pages. Otherwise make sure you exclude the following pages from the cache through your caching systems respective settings.
- Cart
- My Account
- Checkout
These pages need to stay dynamic since they display information specific to the current customer and their cart.
## Excluding WooCommerce Session from the Cache
If the caching system youre using offers database caching, it might be helpful to exclude `_wc_session_` from being cached. This will be dependent on the plugin or host caching so refer to the specific instructions or docs for that system.
## Excluding WooCommerce Cookies from the Cache
Cookies in WooCommerce help track the products in your customers cart, can keep their cart in the database if they leave the site, and powers the recently viewed widget. Below is a list of the cookies WooCommerce uses for this, which you can exclude from caching.
| COOKIE NAME | DURATION | PURPOSE |
| --- | --- | --- |
| woocommerce_cart_hash | session | Helps WooCommerce determine when cart contents/data changes. |
| woocommerce_items_in_cart | session | Helps WooCommerce determine when cart contents/data changes. |
| wp_woocommerce_session_ | 2 days | Contains a unique code for each customer so that it knows where to find the cart data in the database for each customer. |
| woocommerce_recently_viewed | session | Powers the Recent Viewed Products widget. |
| store_notice[notice id] | session | Allows customers to dismiss the Store Notice. |
Were unable to cover all options, but we have added some tips for the popular caching plugins. For more specific support, please reach out to the support team responsible for your caching integration.
### W3 Total Cache Minify Settings
Ensure you add mfunc to the Ignored comment stems option in the Minify settings.
### WP-Rocket
WooCommerce is fully compatible with WP-Rocket. Please ensure that the following pages (Cart, Checkout, My Account) are not to be cached in the plugins settings.
We recommend avoiding JavaScript file minification.
### WP Super Cache
WooCommerce is natively compatible with WP Super Cache. WooCommerce sends information to WP Super Cache so that it doesnt cache the Cart, Checkout, or My Account pages by default.
### Varnish
```varnish
if (req.url ~ "^/(cart|my-account|checkout|addons)") {
return (pass);
}
if ( req.url ~ "\\?add-to-cart=" ) {
return (pass);
}
```
## Troubleshooting
### Why is my Varnish configuration not working in WooCommerce?
Check out the following WordPress.org Support forum post on[ how cookies may be affecting your varnish coding](https://wordpress.org/support/topic/varnish-configuration-not-working-in-woocommerce).
```text
Add this to vcl_recv above "if (req.http.cookie) {":
# Unset Cookies except for WordPress admin and WooCommerce pages
if (!(req.url ~ "(wp-login|wp-admin|cart|my-account/*|wc-api*|checkout|addons|logout|lost-password|product/*)")) {
unset req.http.cookie;
}
# Pass through the WooCommerce dynamic pages
if (req.url ~ "^/(cart|my-account/*|checkout|wc-api/*|addons|logout|lost-password|product/*)") {
return (pass);
}
# Pass through the WooCommerce add to cart
if (req.url ~ "\?add-to-cart=" ) {
return (pass);
}
# Pass through the WooCommerce API
if (req.url ~ "\?wc-api=" ) {
return (pass);
}
# Block access to php admin pages via website
if (req.url ~ "^/phpmyadmin/.*$" || req.url ~ "^/phppgadmin/.*$" || req.url ~ "^/server-status.*$") {
error 403 "For security reasons, this URL is only accesible using localhost (127.0.0.1) as the hostname";
}
#
Add this to vcl_fetch:
# Unset Cookies except for WordPress admin and WooCommerce pages
if ( (!(req.url ~ "(wp-(login|admin)|login|cart|my-account/*|wc-api*|checkout|addons|logout|lost-password|product/*)")) || (req.request == "GET") ) {
unset beresp.http.set-cookie;
}
#
```
### Why is my Password Reset stuck in a loop?
This is due to the My Account page being cached, Some hosts with server-side caching dont prevent my-account.php from being cached.
If youre unable to reset your password and keep being returned to the login screen, please speak to your host to make sure this page is being excluded from their caching.

View File

@ -1,4 +1,4 @@
# WC CLI: Commands
# WC CLI: commands
## wc shop_coupon

View File

@ -1,4 +1,4 @@
# WC CLI: Overview
# WC CLI: overview
WooCommerce CLI (WC-CLI) offers the ability to manage WooCommerce (WC) via the command-line, using WP CLI. The documentation here covers the version of WC CLI that started shipping in WC 3.0.0 and later.

View File

@ -1,3 +1,15 @@
# Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0](https://www.npmjs.com/package/@woocommerce/admin-layout/v/1.0.0) - 2023-11-28
- Patch - Update dependencies.
- Minor - Adding LayoutContext component and hook. [#37720]
- Minor - Adding support for modifying fill name to WooHeaderItem. [#37255]
- Minor - Create @woocommerce/admin-layout package to house header, footer, and similar components and utilities. [#37094]
- Patch - Make eslint emit JSON report for annotating PRs. [#39704]
- Patch - Update webpack config to use @woocommerce/internal-style-build's parser config [#37195]
- Minor - Upgrade TypeScript to 5.1.6 [#39531]
[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/admin-layout/CHANGELOG.md).

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: Just changing package.json command for lint

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Create @woocommerce/admin-layout package to house header, footer, and similar components and utilities.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Adding support for modifying fill name to WooHeaderItem.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Make eslint emit JSON report for annotating PRs.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Upgrade TypeScript to 5.1.6

View File

@ -1,5 +0,0 @@
Significance: patch
Type: dev
Comment: Applied lint auto fixes across monorepo

View File

@ -1,5 +0,0 @@
Significance: patch
Type: dev
Comment: TypeScript build change

View File

@ -1,5 +0,0 @@
Significance: patch
Type: dev
Comment: Configuration change only

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Add missing dev dependency - rimraf

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Adding LayoutContext component and hook.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Update webpack config to use @woocommerce/internal-style-build's parser config

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/admin-layout",
"version": "1.0.0-beta.0",
"version": "1.0.0",
"description": "WooCommerce admin layout copmonents and utilities.",
"author": "Automattic",
"license": "GPL-2.0-or-later",
@ -46,14 +46,15 @@
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/browserslist-config": "wp-6.0",
"concurrently": "^7.0.0",
"css-loader": "^3.6.0",
"eslint": "^8.32.0",
"jest": "^27.5.1",
"jest-cli": "^27.5.1",
"concurrently": "^7.0.0",
"postcss-loader": "^4.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^5.1.6",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add className to the MenuItem component

View File

@ -1,8 +1,9 @@
/**
* External dependencies
*/
import { Tooltip } from '@wordpress/components';
import { createElement, CSSProperties, ReactElement } from 'react';
import classNames from 'classnames';
import { Tooltip } from '@wordpress/components';
/**
* Internal dependencies
@ -17,6 +18,7 @@ export type MenuItemProps< ItemType > = {
getItemProps: getItemPropsType< ItemType >;
activeStyle?: CSSProperties;
tooltipText?: string;
className?: string;
};
export const MenuItem = < ItemType, >( {
@ -27,13 +29,19 @@ export const MenuItem = < ItemType, >( {
activeStyle = { backgroundColor: '#bde4ff' },
item,
tooltipText,
className,
}: MenuItemProps< ItemType > ) => {
function renderListItem() {
const itemProps = getItemProps( { item, index } );
return (
<li
style={ isActive ? activeStyle : {} }
{ ...getItemProps( { item, index } ) }
className="woocommerce-experimental-select-control__menu-item"
{ ...itemProps }
style={ isActive ? activeStyle : itemProps.style }
className={ classNames(
'woocommerce-experimental-select-control__menu-item',
itemProps.className,
className
) }
>
{ children }
</li>

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix parseNumber to allow for emptry string thousand & decimal separators.

View File

@ -135,10 +135,19 @@ export function parseNumber(
const [ , decimals ] = value.split( decimalSeparator );
parsedPrecision = decimals ? decimals.length : 0;
}
let parsedValue = value;
if ( thousandSeparator ) {
parsedValue = parsedValue.replace(
new RegExp( `\\${ thousandSeparator }`, 'g' ),
''
);
}
if ( decimalSeparator ) {
parsedValue = parsedValue.replace(
new RegExp( `\\${ decimalSeparator }`, 'g' ),
'.'
);
}
return Number.parseFloat(
value
.replace( new RegExp( `\\${ thousandSeparator }`, 'g' ), '' )
.replace( new RegExp( `\\${ decimalSeparator }`, 'g' ), '.' )
).toFixed( parsedPrecision );
return Number.parseFloat( parsedValue ).toFixed( parsedPrecision );
}

View File

@ -6,7 +6,7 @@ import { partial } from 'lodash';
/**
* Internal dependencies
*/
import { numberFormat } from '../index';
import { numberFormat, parseNumber } from '../index';
const defaultNumberFormat = partial( numberFormat, {} );
@ -48,3 +48,32 @@ describe( 'numberFormat', () => {
expect( numberFormat( config, '12345.6789' ) ).toBe( '12.345,679' );
} );
} );
describe( 'parseNumber', () => {
it( 'should remove thousand seperator before parsing number', () => {
const config = {
decimalSeparator: ',',
thousandSeparator: '.',
precision: 3,
};
expect( parseNumber( config, '12.345,679' ) ).toBe( '12345.679' );
} );
it( 'supports empty string as the thousandSeperator', () => {
const config = {
decimalSeparator: ',',
thousandSeparator: '',
precision: 3,
};
expect( parseNumber( config, '12345,679' ) ).toBe( '12345.679' );
} );
it( 'supports empty string as the decimalSeperator', () => {
const config = {
decimalSeparator: '',
thousandSeparator: ',',
precision: 2,
};
expect( parseNumber( config, '1,2345,679' ) ).toBe( '12345679.00' );
} );
} );

View File

@ -2,6 +2,12 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0](https://www.npmjs.com/package/@woocommerce/product-editor/v/1.1.0) - 2023-11-28
- Patch - Update internal dependency.
- Minor - Remove downloads list fixed height #41744 [#41744]
- Patch - [Product Block Editor]: remove unused block attributes [#41674]
## [1.0.0](https://www.npmjs.com/package/@woocommerce/product-editor/v/1.0.0) - 2023-11-27
- Patch - Add cursor: not-allowed; to the disabled Quick updates button [#40448]

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product list block

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add ordering support to the product list

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add empty state when no attributes #41679

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
[Product Block Editor]: fix Input control issue in Manage download limit form

View File

@ -1,4 +0,0 @@
Significance: patch
Type: tweak
[Product Block Editor]: remove unused block attributes

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/product-editor",
"version": "1.0.0",
"version": "1.1.0",
"description": "React components for the WooCommerce admin product editor.",
"author": "Automattic",
"license": "GPL-2.0-or-later",

View File

@ -24,6 +24,7 @@ export { init as initToggle } from './generic/toggle';
export { init as attributesInit } from './product-fields/attributes';
export { init as initVariations } from './product-fields/variations';
export { init as initRequirePassword } from './product-fields/password';
export { init as initProductList } from './product-fields/product-list';
export { init as initVariationItems } from './product-fields/variation-items';
export { init as initVariationOptions } from './product-fields/variation-options';
export { init as initNotice } from './product-fields/notice-edit-single-variation';

View File

@ -18,7 +18,6 @@ $fixed-section-height: 224px;
}
&__table {
height: $fixed-section-height;
overflow: auto;
margin: 0;
flex: 1 0 auto;

View File

@ -0,0 +1,27 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-list-field",
"title": "Product list",
"category": "widgets",
"description": "The product list.",
"keywords": [ "products" ],
"textdomain": "default",
"attributes": {
"property": {
"type": "string",
"__experimentalRole": "content"
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false,
"__experimentalToolbar": false
},
"editorStyle": "file:./editor.css",
"usesContext": [ "postType" ]
}

View File

@ -0,0 +1,299 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { useEntityProp } from '@wordpress/core-data';
import { resolveSelect } from '@wordpress/data';
import {
createElement,
useContext,
useEffect,
useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { external, closeSmall } from '@wordpress/icons';
import { useWooBlockProps } from '@woocommerce/block-templates';
import { CurrencyContext } from '@woocommerce/currency';
import { PRODUCTS_STORE_NAME, Product } from '@woocommerce/data';
import { getNewPath } from '@woocommerce/navigation';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import {
AddProductsModal,
getProductImageStyle,
} from '../../../components/add-products-modal';
import { ProductEditorBlockEditProps } from '../../../types';
import { Shirt, Pants, Glasses } from './images';
import { UploadsBlockAttributes } from './types';
import {
getProductStockStatus,
getProductStockStatusClass,
} from '../../../utils';
export function Edit( {
attributes,
context: { postType },
}: ProductEditorBlockEditProps< UploadsBlockAttributes > ) {
const { property } = attributes;
const blockProps = useWooBlockProps( attributes );
const [ openAddProductsModal, setOpenAddProductsModal ] = useState( false );
const [ isLoading, setIsLoading ] = useState( false );
const [ preventFetch, setPreventFetch ] = useState( false );
const [ groupedProductIds, setGroupedProductIds ] = useEntityProp<
number[]
>( 'postType', postType, property );
const [ groupedProducts, setGroupedProducts ] = useState< Product[] >( [] );
const { formatAmount } = useContext( CurrencyContext );
useEffect(
function loadGroupedProducts() {
if ( preventFetch ) return;
if ( groupedProductIds.length ) {
setIsLoading( false );
resolveSelect( PRODUCTS_STORE_NAME )
.getProducts< Product[] >( {
include: groupedProductIds,
orderby: 'include',
} )
.then( setGroupedProducts )
.finally( () => setIsLoading( false ) );
} else {
setGroupedProducts( [] );
}
},
[ groupedProductIds, preventFetch ]
);
function handleAddProductsButtonClick() {
setOpenAddProductsModal( true );
}
function handleAddProductsModalSubmit( value: Product[] ) {
const newGroupedProducts = [ ...groupedProducts, ...value ];
setPreventFetch( true );
setGroupedProducts( newGroupedProducts );
setGroupedProductIds(
newGroupedProducts.map( ( product ) => product.id )
);
setOpenAddProductsModal( false );
}
function handleAddProductsModalClose() {
setOpenAddProductsModal( false );
}
function removeProductHandler( product: Product ) {
return function handleRemoveClick() {
const newGroupedProducts = groupedProducts.filter(
( groupedProduct ) => groupedProduct.id !== product.id
);
setPreventFetch( true );
setGroupedProducts( newGroupedProducts );
setGroupedProductIds(
newGroupedProducts.map(
( groupedProduct ) => groupedProduct.id
)
);
};
}
return (
<div { ...blockProps }>
<div className="wp-block-woocommerce-product-list-field__header">
<Button
onClick={ handleAddProductsButtonClick }
variant="secondary"
>
{ __( 'Add products', 'woocommerce' ) }
</Button>
</div>
<div className="wp-block-woocommerce-product-list-field__body">
{ ! isLoading && groupedProducts.length === 0 && (
<div className="wp-block-woocommerce-product-list-field__empty-state">
<div
className="wp-block-woocommerce-product-list-field__empty-state-illustration"
role="presentation"
>
<Shirt />
<Pants />
<Glasses />
</div>
<p className="wp-block-woocommerce-product-list-field__empty-state-tip">
{ __(
'Tip: Group together items that have a clear relationship or compliment each other well, e.g., garment bundles, camera kits, or skincare product sets.',
'woocommerce'
) }
</p>
</div>
) }
{ ! isLoading && groupedProducts.length > 0 && (
<div
className="wp-block-woocommerce-product-list-field__table"
role="table"
>
<div className="wp-block-woocommerce-product-list-field__table-header">
<div
className="wp-block-woocommerce-product-list-field__table-row"
role="rowheader"
>
<div
className="wp-block-woocommerce-product-list-field__table-header-column"
role="columnheader"
>
{ __( 'Product', 'woocommerce' ) }
</div>
<div
className="wp-block-woocommerce-product-list-field__table-header-column"
role="columnheader"
>
{ __( 'Price', 'woocommerce' ) }
</div>
<div
className="wp-block-woocommerce-product-list-field__table-header-column"
role="columnheader"
>
{ __( 'Stock', 'woocommerce' ) }
</div>
<div
className="wp-block-woocommerce-product-list-field__table-header-column"
role="columnheader"
/>
</div>
</div>
<div
className="wp-block-woocommerce-product-list-field__table-body"
role="rowgroup"
>
{ groupedProducts.map( ( product ) => (
<div
key={ product.id }
className="wp-block-woocommerce-product-list-field__table-row"
role="row"
>
<div
className="wp-block-woocommerce-product-list-field__table-cell"
role="cell"
>
<div
className="wp-block-woocommerce-product-list-field__product-image"
style={ getProductImageStyle(
product
) }
/>
<div className="wp-block-woocommerce-product-list-field__product-info">
<div className="wp-block-woocommerce-product-list-field__product-name">
<Button
variant="link"
href={ getNewPath(
{},
`/product/${ product.id }`
) }
target="_blank"
>
{ product.name }
</Button>
</div>
<div className="wp-block-woocommerce-product-list-field__product-sku">
{ product.sku }
</div>
</div>
</div>
<div
className="wp-block-woocommerce-product-list-field__table-cell"
role="cell"
>
{ product.on_sale && (
<span>
{ product.sale_price
? formatAmount(
product.sale_price
)
: formatAmount(
product.price
) }
</span>
) }
{ product.regular_price && (
<span
className={ classNames( {
'wp-block-woocommerce-product-list-field__price--on-sale':
product.on_sale,
} ) }
>
{ formatAmount(
product.regular_price
) }
</span>
) }
</div>
<div
className="wp-block-woocommerce-product-list-field__table-cell"
role="cell"
>
<span
className={ classNames(
'woocommerce-product-variations__status-dot',
getProductStockStatusClass(
product
)
) }
>
</span>
<span>
{ getProductStockStatus( product ) }
</span>
</div>
<div
className="wp-block-woocommerce-product-list-field__table-cell"
role="cell"
>
<Button
variant="tertiary"
icon={ external }
aria-label={ __(
'Preview the product',
'woocommerce'
) }
href={ product.permalink }
target="_blank"
/>
<Button
type="button"
variant="tertiary"
icon={ closeSmall }
aria-label={ __(
'Remove product',
'woocommerce'
) }
onClick={ removeProductHandler(
product
) }
/>
</div>
</div>
) ) }
</div>
</div>
) }
</div>
{ openAddProductsModal && (
<AddProductsModal
initialValue={ groupedProducts }
onSubmit={ handleAddProductsModalSubmit }
onClose={ handleAddProductsModalClose }
/>
) }
</div>
);
}

View File

@ -0,0 +1,117 @@
.wp-block-woocommerce-product-list-field {
display: flex;
flex-direction: column;
gap: $grid-unit-30;
&__header {
display: flex;
align-items: center;
justify-content: end;
}
&__empty-state {
min-height: 28 * $grid-unit;
border-radius: 2px;
border: 1px dashed $gray-400;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $grid-unit-60;
gap: $grid-unit-30;
&-illustration {
display: flex;
align-items: center;
justify-content: center;
gap: $grid-unit-40;
max-width: 100%;
}
&-tip {
text-align: center;
margin: 0;
color: $gray-700;
}
}
&__table {
&-header {
color: $gray-700;
font-size: 11px;
text-transform: uppercase;
border-bottom: 1px solid $gray-200;
}
&-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: $grid-unit-30;
padding: $grid-unit-20 2px;
&:not(:last-child) {
border-bottom: 1px solid $gray-200;
}
}
&-header-column,
&-cell {
display: flex;
align-items: center;
&:first-child {
grid-column: 1 / span 2;
gap: $grid-unit + $grid-unit-05;
}
&:nth-child(2),
&:last-child {
justify-content: end;
gap: $grid-unit;
}
}
}
&__product-image {
width: $grid-unit-40;
height: $grid-unit-40;
border-radius: $grid-unit-05;
background-color: $gray-200;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
flex-shrink: 0;
}
&__product-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1 1 auto;
overflow: hidden;
}
&__product-name {
color: $gray-900;
.is-link {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&__product-sku {
color: $gray-700;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__price--on-sale {
color: $gray-600;
text-decoration: line-through;
}
}

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function Glasses() {
return (
<svg
width="72"
height="33"
viewBox="0 0 72 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.82318 26.2927C2.40837 25.6395 12.1796 15.8683 13.4588 14.6027C14.6156 13.4596 16.0853 12.9696 17.8409 12.8336C19.2426 12.7383 65.159 9.36328 65.159 9.36328L65.9075 12.9016C65.9075 12.9016 17.3782 17.7736 17.3509 17.7736C13.5132 22.8633 8.01523 30.7156 7.49809 31.3008C6.10999 32.8658 3.71482 33.0019 2.13618 31.6274C0.598377 30.2257 0.448677 27.8306 1.83679 26.2655L1.82318 26.2927Z"
fill="#F0F0F0"
/>
<path
d="M70.4378 26.2927C69.8526 25.6395 60.0815 15.8683 58.8022 14.6027C57.6455 13.4596 56.1757 12.9696 54.4202 12.8336C53.0184 12.7383 7.10201 9.36328 7.10201 9.36328L6.35352 12.9016C6.35352 12.9016 54.8829 17.7736 54.9101 17.7736C58.7478 22.8633 64.2458 30.7156 64.7629 31.3008C66.151 32.8658 68.5462 33.0019 70.1248 31.6274C71.6626 30.2257 71.8123 27.8306 70.4242 26.2655L70.4378 26.2927Z"
fill="#F0F0F0"
/>
<path
d="M53.3189 0C46.4328 0 41.6016 2.5993 38.4715 8.70969C38.1449 8.4103 37.1379 8.01564 36.1172 8.01564C35.0965 8.01564 34.1031 8.42391 33.7629 8.70969C30.6328 2.5993 25.8017 0 18.9156 0C12.0295 0 4.49012 4.53176 0.666016 6.38257V11.9078H4.77591C5.34748 15.4461 8.61362 25.68 19.0244 25.68C28.1288 25.68 30.9322 19.2838 32.9736 15.378C33.6812 14.0172 34.5658 12.0711 36.1172 12.0711C37.6686 12.0711 38.5532 14.0172 39.2609 15.378C41.2886 19.2838 44.092 25.68 53.21 25.68C63.6208 25.68 66.8869 15.4597 67.4585 11.9078H71.5684V6.38257C67.7443 4.54537 60.3683 0 53.3189 0ZM19.0517 22.3186C12.9277 22.3186 8.83136 16.9022 8.83136 11.4723C8.83136 5.53882 14.1797 3.2117 19.1605 3.2117C24.1414 3.2117 29.8027 5.811 29.8027 11.0912C29.8027 17.1336 25.3662 22.305 19.0517 22.305V22.3186ZM53.21 22.3186C46.9091 22.3186 42.459 17.1472 42.459 11.1049C42.459 5.83822 48.1066 3.22531 53.1011 3.22531C58.0956 3.22531 63.4303 5.55243 63.4303 11.4859C63.4303 16.9159 59.3204 22.3322 53.21 22.3322V22.3186Z"
fill="#E0E0E0"
/>
</svg>
);
}

View File

@ -0,0 +1,3 @@
export * from './glasses';
export * from './pants';
export * from './shirt';

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
export function Pants() {
const clipPathId = useInstanceId( Pants, 'pants' ) as string;
return (
<svg
width="50"
height="72"
viewBox="0 0 50 72"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath={ `url(#${ clipPathId })` }>
<path
d="M44.6084 21.3845C40.788 21.6427 35.5059 20.8456 35.1404 16.333C34.8746 13.0889 34.5867 9.04771 34.3431 5.7811H42.9474L42.3273 0H8.34205L7.72192 5.7811H16.3262C16.0826 9.04771 15.8057 13.0889 15.5289 16.333C15.1635 20.8456 9.87022 21.6314 6.06086 21.3845L0.667969 72H14.0007C14.0007 72 21.7745 32.0711 22.904 26.0318C23.4909 22.9111 24.3989 22.2264 25.3291 22.2264C26.2593 22.2264 27.1673 22.9224 27.7543 26.0318C28.8948 32.0599 36.6575 72 36.6575 72H49.9903L44.5974 21.3845H44.6084Z"
fill="#F0F0F0"
/>
<path
d="M15.5383 16.3332C15.8041 13.089 16.092 9.04785 16.3356 5.78125H7.73137L6.07031 21.3846C9.89074 21.6428 15.1729 20.8458 15.5383 16.3332Z"
fill="#DDDDDD"
/>
<path
d="M35.1293 16.3332C35.4948 20.8458 40.788 21.6316 44.5974 21.3846L42.9363 5.78125H34.332C34.5757 9.04785 34.8525 13.089 35.1293 16.3332Z"
fill="#DDDDDD"
/>
</g>
<defs>
<clipPath id={ clipPathId }>
<rect
width="49.3334"
height="72"
fill="white"
transform="translate(0.667969)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function Shirt() {
return (
<svg
width="68"
height="56"
viewBox="0 0 68 56"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M43.0926 0.333984C41.0526 1.54732 37.5593 2.46732 34.2526 2.46732C30.946 2.46732 27.4526 1.54732 25.4126 0.333984L22.2793 10.5207H46.2126L43.106 0.333984H43.0926Z"
fill="#E0E0E0"
/>
<path
d="M43.0927 0.333984C43.0927 4.09398 40.306 8.80065 34.2527 8.80065C28.1994 8.80065 25.4127 4.08065 25.4127 0.333984C15.546 0.333984 3.81268 7.45398 0.666016 10.6006L9.73269 24.7606L14.986 23.414L15.066 55.5606H53.4394L53.5194 23.414L58.7727 24.7606L67.8394 10.6006C64.6927 7.45398 52.9594 0.333984 43.0927 0.333984Z"
fill="#F0F0F0"
/>
</svg>
);
}

View File

@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import blockConfiguration from './block.json';
import { Edit } from './edit';
import { registerProductEditorBlockType } from '../../../utils';
const { name, ...metadata } = blockConfiguration;
export { metadata, name };
export const settings = {
example: {},
edit: Edit,
};
export function init() {
return registerProductEditorBlockType( {
name,
metadata: metadata as never,
settings: settings as never,
} );
}

View File

@ -0,0 +1,8 @@
/**
* External dependencies
*/
import { BlockAttributes } from '@wordpress/blocks';
export interface UploadsBlockAttributes extends BlockAttributes {
property: string;
}

View File

@ -4,8 +4,8 @@
@import "product-fields/inventory-email/editor.scss";
@import "product-fields/inventory-sku/editor.scss";
@import "product-fields/name/editor.scss";
@import 'product-fields/notice-has-variations/editor.scss';
@import 'product-fields/notice-edit-single-variation/editor.scss';
@import "product-fields/notice-has-variations/editor.scss";
@import "product-fields/notice-edit-single-variation/editor.scss";
@import "generic/pricing/editor.scss";
@import "product-fields/regular-price/editor.scss";
@import "product-fields/sale-price/editor.scss";
@ -16,6 +16,7 @@
@import "generic/tab/editor.scss";
@import "product-fields/variations/editor.scss";
@import "product-fields/password/editor.scss";
@import "product-fields/product-list/editor.scss";
@import "product-fields/variation-items/editor.scss";
@import "product-fields/variation-options/editor.scss";
@import "generic/taxonomy/editor.scss";

View File

@ -0,0 +1,279 @@
/**
* External dependencies
*/
import { FormEvent, useEffect } from 'react';
import { Button, Modal, Spinner } from '@wordpress/components';
import { resolveSelect } from '@wordpress/data';
import {
createElement,
Fragment,
useContext,
useCallback,
useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { closeSmall, dragHandle } from '@wordpress/icons';
import {
__experimentalSelectControl as SelectControl,
__experimentalSelectControlMenu as Menu,
__experimentalSelectControlMenuItem as MenuItem,
useAsyncFilter,
} from '@woocommerce/components';
import { CurrencyContext } from '@woocommerce/currency';
import { PRODUCTS_STORE_NAME, Product } from '@woocommerce/data';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { AddProductsModalProps } from './types';
import { useDraggable } from '../../hooks/use-draggable';
export function getProductImageStyle( product: Product ) {
return product.images.length > 0
? {
backgroundImage: `url(${ product.images[ 0 ].src })`,
}
: undefined;
}
export function AddProductsModal( {
initialValue,
onSubmit,
onClose,
}: AddProductsModalProps ) {
const [ products, setProducts ] = useState< Product[] >( [] );
const [ selectedProducts, setSelectedProducts ] = useState< Product[] >(
[]
);
function handleSubmit( event: FormEvent< HTMLFormElement > ) {
event.preventDefault();
onSubmit( [ ...selectedProducts ] );
}
function handleCancelClick() {
onClose();
}
const filter = useCallback(
async ( search = '' ) => {
setProducts( [] );
return resolveSelect( PRODUCTS_STORE_NAME )
.getProducts< Product[] >( {
search,
orderby: 'title',
order: 'asc',
exclude: [ ...initialValue, ...selectedProducts ].map(
( product ) => product.id
),
} )
.then( ( response ) => {
setProducts( response );
return response;
} );
},
[ selectedProducts ]
);
const { isFetching, ...selectProps } = useAsyncFilter< Product >( {
filter,
} );
useEffect(
function preloadProducts() {
filter();
},
[ initialValue, selectedProducts ]
);
function handleSelect( value: Product ) {
setSelectedProducts( ( current ) => [ ...current, value ] );
}
const { formatAmount } = useContext( CurrencyContext );
function removeProductHandler( product: Product ) {
return function handleRemoveClick() {
setSelectedProducts( ( current ) =>
current.filter( ( item ) => item.id !== product.id )
);
};
}
const { container, draggable, handler } = useDraggable( {
onSort: setSelectedProducts,
} );
return (
<Modal
title={ __( 'Add products to this group', 'woocommerce' ) }
className="woocommerce-add-products-modal"
onRequestClose={ onClose }
>
<form
noValidate
onSubmit={ handleSubmit }
className="woocommerce-add-products-modal__form"
>
<fieldset className="woocommerce-add-products-modal__form-group">
<legend className="woocommerce-add-products-modal__form-group-title">
{ __(
'Add and manage products in this group to let customers purchase them all in one go.',
'woocommerce'
) }
</legend>
<div className="woocommerce-add-products-modal__form-group-content">
<SelectControl< Product >
{ ...selectProps }
items={ products }
placeholder={ __(
'Search for products',
'woocommerce'
) }
label=""
selected={ null }
onSelect={ handleSelect }
__experimentalOpenMenuOnFocus
>
{ ( {
items,
isOpen,
highlightedIndex,
getMenuProps,
getItemProps,
} ) => (
<Menu
isOpen={ isOpen }
getMenuProps={ getMenuProps }
className="woocommerce-add-products-modal__menu"
>
{ isFetching ? (
<div className="woocommerce-add-products-modal__menu-loading">
<Spinner />
</div>
) : (
items.map( ( item, index ) => (
<MenuItem< Product >
key={ item.id }
index={ index }
isActive={
highlightedIndex === index
}
item={ item }
getItemProps={ (
options
) => ( {
...getItemProps( options ),
className:
'woocommerce-add-products-modal__menu-item',
} ) }
>
<>
<div
className="woocommerce-add-products-modal__menu-item-image"
style={ getProductImageStyle(
item
) }
/>
<div className="woocommerce-add-products-modal__menu-item-content">
<div className="woocommerce-add-products-modal__menu-item-title">
{ item.name }
</div>
{ Boolean(
item.price
) && (
<div className="woocommerce-add-products-modal__menu-item-description">
{ formatAmount(
item.price
) }
</div>
) }
</div>
</>
</MenuItem>
) )
) }
</Menu>
) }
</SelectControl>
</div>
{ Boolean( selectedProducts.length ) && (
<ul
{ ...container }
className={ classNames(
'woocommerce-add-products-modal__list',
container.className
) }
>
{ selectedProducts.map( ( item ) => (
<li
{ ...draggable }
key={ item.id }
className="woocommerce-add-products-modal__list-item"
>
<Button
{ ...handler }
icon={ dragHandle }
variant="tertiary"
type="button"
aria-label={ __(
'Sortable handler',
'woocommerce'
) }
/>
<div
className="woocommerce-add-products-modal__list-item-image"
style={ getProductImageStyle( item ) }
/>
<div className="woocommerce-add-products-modal__list-item-content">
<div className="woocommerce-add-products-modal__list-item-title">
{ item.name }
</div>
<div className="woocommerce-add-products-modal__list-item-description">
{ item.sku }
</div>
</div>
<div className="woocommerce-add-products-modal__list-item-actions">
<Button
type="button"
variant="tertiary"
icon={ closeSmall }
aria-label={ __(
'Remove product',
'woocommerce'
) }
onClick={ removeProductHandler(
item
) }
/>
</div>
</li>
) ) }
</ul>
) }
</fieldset>
<div className="woocommerce-add-products-modal__actions">
<Button
variant="tertiary"
type="button"
onClick={ handleCancelClick }
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button variant="primary" type="submit">
{ __( 'Add', 'woocommerce' ) }
</Button>
</div>
</form>
</Modal>
);
}

View File

@ -0,0 +1,2 @@
export * from './add-products-modal';
export * from './types';

View File

@ -0,0 +1,143 @@
.woocommerce-add-products-modal {
@include breakpoint(">600px") {
width: calc(100% - 32px);
}
@include breakpoint(">782px") {
width: 640px;
.components-input-control__container {
width: 50%;
}
}
&__input-suffix {
margin-right: $grid-unit-15;
}
&__form {
&-group {
&-title {
padding: 0;
width: 100%;
margin-bottom: $grid-unit-40;
}
&-content {
display: flex;
flex-direction: column;
gap: $grid-unit + $grid-unit-05;
.components-base-control {
.components-input-control__container {
.components-input-control__input {
min-height: 36px;
}
}
}
.components-base-control.has-error {
.components-input-control__backdrop {
border-color: $studio-red-50;
}
.components-base-control__help {
color: $studio-red-50;
}
}
}
}
}
&__actions {
margin-top: $grid-unit-30;
display: flex;
flex-direction: row;
gap: 8px;
justify-content: flex-end;
}
&__menu {
&-loading {
display: flex;
align-items: center;
justify-content: center;
padding: $grid-unit;
height: $grid-unit-60 + 2px;
}
&-item {
display: flex;
align-items: center;
padding: $grid-unit;
gap: $grid-unit + $grid-unit-05;
&-image {
width: $grid-unit-40;
height: $grid-unit-40;
border-radius: $grid-unit-05;
background-color: $gray-200;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
flex-shrink: 0;
}
&-content {
display: flex;
flex-direction: column;
gap: 2px;
}
&-title {
color: $gray-900;
}
&-description {
color: $gray-700;
font-size: 11px;
}
}
}
&__list {
&-item {
display: flex;
align-items: center;
padding: $grid-unit-20 0;
gap: $grid-unit + $grid-unit-05;
margin: 0;
&:not(:last-child) {
border-bottom: 1px solid $gray-100;
}
&-image {
width: $grid-unit-40;
height: $grid-unit-40;
border-radius: $grid-unit-05;
background-color: $gray-200;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
flex-shrink: 0;
}
&-content {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1 1 auto;
}
&-title {
color: $gray-900;
}
&-description {
color: $gray-700;
font-size: 11px;
}
}
}
}

View File

@ -0,0 +1,10 @@
/**
* External dependencies
*/
import { Product } from '@woocommerce/data';
export type AddProductsModalProps = {
initialValue: Product[];
onSubmit( value: Product[] ): void;
onClose(): void;
};

View File

@ -7,6 +7,9 @@
&__add-new-icon {
margin-right: $gap-small;
}
&__no-results {
padding: $gap-small;
}
}
.woocommerce-experimental-select-control__popover-menu-container {

View File

@ -1,15 +1,12 @@
/**
* External dependencies
*/
import { sprintf, __ } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { Spinner, Icon } from '@wordpress/components';
import { plus } from '@wordpress/icons';
import { Spinner } from '@wordpress/components';
import { createElement, useMemo } from '@wordpress/element';
import {
EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME,
QueryProductAttribute,
ProductAttribute,
WCDataSelector,
ProductAttributesActions,
WPDataActions,
@ -18,43 +15,21 @@ import { recordEvent } from '@woocommerce/tracks';
import {
__experimentalSelectControl as SelectControl,
__experimentalSelectControlMenu as Menu,
__experimentalSelectControlMenuItem as MenuItem,
} from '@woocommerce/components';
/**
* Internal dependencies
*/
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
import { TRACKS_SOURCE } from '../../constants';
type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' > & {
slug?: string;
isDisabled?: boolean;
};
type AttributeInputFieldProps = {
value?: EnhancedProductAttribute | null;
onChange: (
value?:
| Omit< ProductAttribute, 'position' | 'visible' | 'variation' >
| string
) => void;
label?: string;
placeholder?: string;
disabled?: boolean;
disabledAttributeIds?: number[];
disabledAttributeMessage?: string;
ignoredAttributeIds?: number[];
createNewAttributesAsGlobal?: boolean;
};
function isNewAttributeListItem( attribute: NarrowedQueryAttribute ): boolean {
return attribute.id === -99;
}
function sanitizeSlugName( slug: string | undefined ): string {
return slug && slug.startsWith( 'pa_' ) ? slug.substring( 3 ) : '';
}
import { MenuAttributeList } from './menu-attribute-list';
import {
AttributeInputFieldProps,
getItemPropsType,
getMenuPropsType,
NarrowedQueryAttribute,
UseComboboxGetItemPropsOptions,
UseComboboxGetMenuPropsOptions,
} from './types';
export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
value = null,
@ -86,7 +61,7 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
const markedAttributes = useMemo(
function setDisabledAttribute() {
return (
attributes?.map( ( attribute ) => ( {
attributes?.map( ( attribute: NarrowedQueryAttribute ) => ( {
...attribute,
isDisabled: disabledAttributeIds.includes( attribute.id ),
} ) ) ?? []
@ -95,6 +70,12 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
[ attributes, disabledAttributeIds ]
);
function isNewAttributeListItem(
attribute: NarrowedQueryAttribute
): boolean {
return attribute.id === -99;
}
const getFilteredItems = (
allItems: NarrowedQueryAttribute[],
inputValue: string
@ -175,7 +156,7 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
getItemLabel={ ( item ) => item?.name || '' }
getItemValue={ ( item ) => item?.id || '' }
selected={ value }
onSelect={ ( attribute ) => {
onSelect={ ( attribute: NarrowedQueryAttribute ) => {
if ( isNewAttributeListItem( attribute ) ) {
addNewAttribute( attribute );
} else {
@ -196,51 +177,33 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
getItemProps,
getMenuProps,
isOpen,
}: {
items: NarrowedQueryAttribute[];
highlightedIndex: number;
getItemProps: (
options: UseComboboxGetItemPropsOptions< NarrowedQueryAttribute >
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => any;
getMenuProps: getMenuPropsType;
isOpen: boolean;
} ) => {
return (
<Menu getMenuProps={ getMenuProps } isOpen={ isOpen }>
{ isLoading ? (
<Spinner />
) : (
renderItems.map( ( item, index: number ) => (
<MenuItem
key={ item.id }
index={ index }
isActive={ highlightedIndex === index }
item={ item }
getItemProps={ ( options ) => ( {
...getItemProps( options ),
disabled: item.isDisabled || undefined,
} ) }
tooltipText={
item.isDisabled
? disabledAttributeMessage
: sanitizeSlugName( item.slug )
}
>
{ isNewAttributeListItem( item ) ? (
<div className="woocommerce-attribute-input-field__add-new">
<Icon
icon={ plus }
size={ 20 }
className="woocommerce-attribute-input-field__add-new-icon"
/>
<span>
{ sprintf(
/* translators: The name of the new attribute term to be created */
__(
'Create "%s"',
'woocommerce'
),
item.name
) }
</span>
</div>
) : (
item.name
) }
</MenuItem>
) )
<MenuAttributeList
renderItems={ renderItems }
highlightedIndex={ highlightedIndex }
disabledAttributeMessage={
disabledAttributeMessage
}
getItemProps={
getItemProps as (
options: UseComboboxGetMenuPropsOptions
) => getItemPropsType< NarrowedQueryAttribute >
}
/>
) }
</Menu>
);

View File

@ -0,0 +1,82 @@
/**
* External dependencies
*/
import { sprintf, __ } from '@wordpress/i18n';
import { plus } from '@wordpress/icons';
import { Icon } from '@wordpress/components';
import { createElement, Fragment } from '@wordpress/element';
import { __experimentalSelectControlMenuItem as MenuItem } from '@woocommerce/components';
/**
* Internal dependencies
*/
import {
MenuAttributeListProps,
NarrowedQueryAttribute,
UseComboboxGetMenuPropsOptions,
} from './types';
function isNewAttributeListItem( attribute: NarrowedQueryAttribute ): boolean {
return attribute.id === -99;
}
function sanitizeSlugName( slug: string | undefined ): string {
return slug && slug.startsWith( 'pa_' ) ? slug.substring( 3 ) : '';
}
export const MenuAttributeList: React.FC< MenuAttributeListProps > = ( {
disabledAttributeMessage = '',
renderItems,
highlightedIndex,
getItemProps,
} ) => {
if ( renderItems.length > 0 ) {
return (
<Fragment>
{ renderItems.map( ( item, index: number ) => (
<MenuItem
key={ item.id }
index={ index }
isActive={ highlightedIndex === index }
item={ item }
getItemProps={ (
options: UseComboboxGetMenuPropsOptions
) => ( {
...getItemProps( options ),
disabled: item.isDisabled || undefined,
} ) }
tooltipText={
item.isDisabled
? disabledAttributeMessage
: sanitizeSlugName( item.slug )
}
>
{ isNewAttributeListItem( item ) ? (
<div className="woocommerce-attribute-input-field__add-new">
<Icon
icon={ plus }
size={ 20 }
className="woocommerce-attribute-input-field__add-new-icon"
/>
<span>
{ sprintf(
/* translators: The name of the new attribute term to be created */
__( 'Create "%s"', 'woocommerce' ),
item.name
) }
</span>
</div>
) : (
item.name
) }
</MenuItem>
) ) }
</Fragment>
);
}
return (
<div className="woocommerce-attribute-input-field__no-results">
{ __( 'Nothing yet. Type to create.', 'woocommerce' ) }
</div>
);
};

View File

@ -0,0 +1,82 @@
/**
* External dependencies
*/
import { QueryProductAttribute, ProductAttribute } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
export type NarrowedQueryAttribute = Pick<
QueryProductAttribute,
'id' | 'name'
> & {
slug?: string;
isDisabled?: boolean;
};
export type AttributeInputFieldProps = {
value?: EnhancedProductAttribute | null;
onChange: (
value?:
| Omit< ProductAttribute, 'position' | 'visible' | 'variation' >
| string
) => void;
label?: string;
placeholder?: string;
disabled?: boolean;
disabledAttributeIds?: number[];
disabledAttributeMessage?: string;
ignoredAttributeIds?: number[];
createNewAttributesAsGlobal?: boolean;
};
export type MenuAttributeListProps = {
renderItems: NarrowedQueryAttribute[];
highlightedIndex: number;
disabledAttributeMessage?: string;
getItemProps: (
options: UseComboboxGetMenuPropsOptions
) => getItemPropsType< NarrowedQueryAttribute >;
};
export interface GetPropsWithRefKey {
refKey?: string;
}
export interface GetMenuPropsOptions
extends React.HTMLProps< HTMLElement >,
GetPropsWithRefKey {
[ 'aria-label' ]?: string;
}
export interface UseComboboxGetMenuPropsOptions
extends GetPropsWithRefKey,
GetMenuPropsOptions {}
export interface GetPropsCommonOptions {
suppressRefError?: boolean;
}
export interface GetItemPropsOptions< Item >
extends React.HTMLProps< HTMLElement > {
index?: number;
item: Item;
isSelected?: boolean;
disabled?: boolean;
}
export interface UseComboboxGetItemPropsOptions< Item >
extends GetItemPropsOptions< Item >,
GetPropsWithRefKey {}
export type getMenuPropsType = (
options?: UseComboboxGetMenuPropsOptions,
otherOptions?: GetPropsCommonOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => any;
export type getItemPropsType< ItemType > = (
options: UseComboboxGetItemPropsOptions< ItemType >
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => any;

View File

@ -51,3 +51,8 @@ export {
TextControl as __experimentalTextControl,
TextControlProps,
} from './text-control';
export {
AddProductsModal as __experimentalAddProductsModal,
AddProductsModalProps,
} from './add-products-modal';

View File

@ -122,11 +122,14 @@ export function ManageDownloadLimitsModal( {
return true;
}
const downloadLimitInputProps = useNumberInputProps( {
value: downloadLimit,
onChange: setDownloadLimit,
} );
const downloadLimitProps = {
...useNumberInputProps( {
value: downloadLimit,
onChange: setDownloadLimit,
} ),
value: downloadLimitInputProps.value,
onChange: downloadLimitInputProps.onChange,
id: useInstanceId(
BaseControl,
'product_download_limit_field'
@ -154,11 +157,14 @@ export function ManageDownloadLimitsModal( {
},
};
const downloadExpiryInputProps = useNumberInputProps( {
value: downloadExpiry,
onChange: setDownloadExpiry,
} );
const downloadExpiryProps = {
...useNumberInputProps( {
value: downloadExpiry,
onChange: setDownloadExpiry,
} ),
value: downloadExpiryInputProps.value,
onChange: downloadExpiryInputProps.onChange,
id: useInstanceId(
BaseControl,
'product_download_expiry_field'

View File

@ -0,0 +1 @@
export * from './use-draggable';

View File

@ -0,0 +1,41 @@
.woocommerce-draggable {
&__container {
[data-draggable="target"] {
&.is-dragging {
opacity: 0.5;
background-color: $white;
border-radius: $grid-unit-05;
}
&.is-dragging-before,
&.is-dragging-after {
position: relative;
}
&.is-dragging-before:before,
&.is-dragging-after:after {
content: "";
display: block;
position: absolute;
width: 100%;
height: $grid-unit-05 + 1px;
left: 0;
background-color: var(--wp-admin-theme-color);
border-radius: $grid-unit-30;
}
&.is-dragging-before:before {
bottom: 100%;
}
&.is-dragging-after:after {
top: 100%;
}
}
[data-draggable="handler"] {
cursor: grab;
user-select: none;
}
}
}

View File

@ -0,0 +1,3 @@
export type DraggableProps< T > = {
onSort( fnState: ( items: T[] ) => T[] ): void;
};

View File

@ -0,0 +1,164 @@
/**
* External dependencies
*/
import { DragEvent, MouseEvent } from 'react';
import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import { DraggableProps } from './types';
import { findDraggableIndex, sort } from './utils';
export function useDraggable< T >( { onSort }: DraggableProps< T > ) {
const dragIndexRef = useRef< number >( -1 );
const dropIndexRef = useRef< number >( -1 );
const draggableElementsRef = useRef< HTMLElement[] >( [] );
function onDragStart( event: DragEvent< HTMLElement > ) {
const element = event.target as HTMLElement;
if ( element.dataset.draggable !== 'target' ) {
event.preventDefault();
return;
}
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.dropEffect = 'move';
element.classList.add( 'is-dragging' );
const parent = element.closest( '[data-draggable=parent]' );
draggableElementsRef.current = Array.from(
parent
?.querySelectorAll< HTMLElement >( '[data-draggable=target]' )
?.values() ?? []
);
dragIndexRef.current = draggableElementsRef.current.indexOf( element );
}
function onDragEnd( event: DragEvent< HTMLElement > ) {
const element = event.target as HTMLElement;
if ( element.dataset.draggable !== 'target' ) {
event.preventDefault();
return;
}
element.classList.remove( 'is-dragging' );
}
function onDragEnter( event: DragEvent< HTMLElement > ) {
const element = event.target as HTMLElement;
const relatedTarget = event.relatedTarget as HTMLElement | null;
if (
element.dataset.draggable !== 'target' ||
element.contains( relatedTarget )
) {
event.preventDefault();
return;
}
const { draggable, index } = findDraggableIndex(
draggableElementsRef.current,
element
);
dropIndexRef.current = index;
if ( dragIndexRef.current === dropIndexRef.current ) return;
if ( dragIndexRef.current < dropIndexRef.current ) {
draggable?.classList.add( 'is-dragging-after' );
} else {
draggable?.classList.add( 'is-dragging-before' );
}
}
function onDragLeave( event: DragEvent< HTMLElement > ) {
const element = event.target as HTMLElement;
const relatedTarget = event.relatedTarget as HTMLElement | null;
if (
element.dataset.draggable !== 'target' ||
element.contains( relatedTarget )
) {
event.preventDefault();
return;
}
element.classList.remove( 'is-dragging-before' );
element.classList.remove( 'is-dragging-after' );
}
function onDrop( event: DragEvent< HTMLElement > ) {
event.preventDefault();
const element = event.target as HTMLElement;
const draggable =
element.dataset.draggable === 'target'
? element
: element.closest(
'[data-draggable=parent] [data-draggable=target]'
);
draggable?.removeAttribute( 'draggable' );
draggable?.classList.remove( 'is-dragging-before' );
draggable?.classList.remove( 'is-dragging-after' );
if (
dragIndexRef.current !== -1 &&
dropIndexRef.current !== -1 &&
dragIndexRef.current !== dropIndexRef.current
) {
const drapIndex = dragIndexRef.current;
const dropIndex = dropIndexRef.current;
onSort( ( items: T[] ) =>
sort(
items,
drapIndex,
dropIndex + Number( drapIndex < dropIndex )
)
);
}
dragIndexRef.current = -1;
dropIndexRef.current = -1;
}
function onDragOver( event: DragEvent< HTMLElement > ) {
event.preventDefault();
return false;
}
function onMouseDown( event: MouseEvent< HTMLElement > ) {
const element = event.target as HTMLElement;
element
.closest( '[data-draggable=parent] [data-draggable=target]' )
?.setAttribute( 'draggable', 'true' );
}
function onMouseUp( event: MouseEvent< HTMLElement > ) {
const element = event.target as HTMLElement;
element
.closest( '[data-draggable=parent] [data-draggable=target]' )
?.removeAttribute( 'draggable' );
}
return {
container: {
'data-draggable': 'parent',
className: 'woocommerce-draggable__container',
},
draggable: {
'data-draggable': 'target',
onDragStart,
onDragEnter,
onDragOver,
onDragLeave,
onDragEnd,
onDrop,
},
handler: {
'data-draggable': 'handler',
onMouseDown,
onMouseUp,
onMouseLeave: onMouseUp,
},
};
}

View File

@ -0,0 +1,35 @@
export function findDraggableIndex(
draggableElements: HTMLElement[],
element: HTMLElement
) {
const index = draggableElements.findIndex(
( child ) => child === element || child.contains( element )
);
return {
draggable: index >= 0 ? draggableElements[ index ] : undefined,
index,
};
}
export function sort< T >(
items: T[],
currentIndex: number,
newIndex: number
): T[] {
const currentItem = items[ currentIndex ];
const newItems = items.reduce< T[] >( ( current, item, index ) => {
if ( index !== currentIndex ) {
if ( index === newIndex ) {
current.push( currentItem );
}
current.push( item );
}
return current;
}, [] );
if ( newIndex >= items.length ) {
newItems.push( currentItem );
}
return newItems;
}

View File

@ -38,7 +38,11 @@
@import "components/modal-editor-welcome-guide/style.scss";
@import "components/attribute-control/attribute-skeleton.scss";
@import "components/checkbox-control/style.scss";
@import "components/add-products-modal/style.scss";
/* Field Blocks */
@import "blocks/style.scss";
/* Hooks */
@import "hooks/use-draggable/styles.scss";

View File

@ -52,9 +52,7 @@ export const Products = () => {
} );
const { productTypes: productTypeListItems } = useProductTypeListItems(
getProductTypes( {
exclude: [ 'subscription' ],
} ),
getProductTypes(),
[],
{
onClick: recordCompletionTime,

View File

@ -5,7 +5,6 @@ import { __ } from '@wordpress/i18n';
import ProductIcon from 'gridicons/dist/product';
import CloudOutlineIcon from 'gridicons/dist/cloud-outline';
import TypesIcon from 'gridicons/dist/types';
import CalendarIcon from 'gridicons/dist/calendar';
import { Icon, chevronRight } from '@wordpress/icons';
/**
@ -46,16 +45,6 @@ export const productTypes = Object.freeze( [
before: <TypesIcon />,
after: <Icon icon={ chevronRight } />,
},
{
key: 'subscription' as const,
title: __( 'Subscription product', 'woocommerce' ),
content: __(
'Item that customers receive on a regular basis.',
'woocommerce'
),
before: <CalendarIcon />,
after: <Icon icon={ chevronRight } />,
},
{
key: 'grouped' as const,
title: __( 'Grouped product', 'woocommerce' ),
@ -93,23 +82,11 @@ export const onboardingProductTypesToSurfaced: Readonly<
Record< string, ProductTypeKey[] >
> = Object.freeze( {
physical: [ 'physical', 'variable', 'grouped' ],
subscriptions: [ 'subscription' ],
downloads: [ 'digital' ],
// key in alphabetical and ascending order for mapping
'physical,subscriptions': [ 'physical', 'subscription' ],
'downloads,physical': [ 'physical', 'digital' ],
'downloads,subscriptions': [ 'digital', 'subscription' ],
'downloads,physical,subscriptions': [
'physical',
'digital',
'subscription',
],
} );
export const defaultSurfacedProductTypes =
onboardingProductTypesToSurfaced.physical;
export const supportedOnboardingProductTypes = [
'physical',
'subscriptions',
'downloads',
];
export const supportedOnboardingProductTypes = [ 'physical', 'downloads' ];

Some files were not shown because too many files have changed in this diff Show More