Merge branch 'trunk' into update/rename-edit-fn-names
This commit is contained in:
commit
ac18ae7db4
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Naming Conventions
|
||||
# Naming conventions
|
||||
|
||||
## PHP
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# WooCommerce Git Flow
|
||||
# WooCommerce Git flow
|
||||
|
||||
For core development, we use the following structure and flow.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Data Stores
|
||||
# Data stores
|
||||
|
||||
## Introduction
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
# Adding a Section to a Settings Tab
|
||||
# Adding a section to a settings tab
|
||||
|
||||
When you’re adding building an extension for WooCommerce that requires settings of some kind, it’s 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.
|
||||
|
||||
|
|
|
@ -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/).
|
||||
|
||||
|
|
|
@ -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 plugin’s 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.
|
|
@ -1,4 +1,4 @@
|
|||
# Implementing Settings for Extensions
|
||||
# Implementing settings for extensions
|
||||
|
||||
If you’re customizing WooCommerce or adding your own functionality to it you’ll 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. We’ve created this tutorial so you can see how to create a new integration.
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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 it’s 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, we’ll 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 theme’s `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 you’re using FTP, it means that you go directly to the folders of your website. That means you’ll need **FTP access** to your host, so you can upload the new child theme. If you don’t 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 you’ve 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 doesn’t hold any customization, so let’s look at a couple of examples of how we can customize the child theme without touching the parent theme.
|
||||
|
||||
### Design customization
|
||||
|
||||
Let’s 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 doesn’t apply to Storefront child themes. Any customizations to a Storefront child theme’s files will be lost when updating. Instead of customizing the Storefront child theme’s files directly, we recommended that you add code snippets to a customization plugin. We’ve created one to do just this. Download [Theme Customizations](https://github.com/woocommerce/theme-customisations) for free.
|
||||
|
||||
But wait, there’s 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 theme’s `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 WooCommerce’s 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 theme’s functions.php.
|
||||
|
||||
The `functions.php` in your child theme is loaded **before** the parent theme’s `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 theme’s `functions.php` and have it replace the one in your parent theme. The only requirement is that the parent theme’s 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.
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# CSS/Sass Naming Conventions
|
||||
# CSS/Sass naming conventions
|
||||
|
||||
Table of Contents:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Naming Conventions
|
||||
# Naming conventions
|
||||
|
||||
Table of Contents:
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -1,4 +1,4 @@
|
|||
# Extending WC-Admin reports
|
||||
# Extending WooCommerce Analytics reports
|
||||
|
||||
## Introduction
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 doesn’t 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 theme’s `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 **WooCommerce’s loop instead**. Save the file. You’re done.
|
||||
|
||||
**Note:** When creating `woocommerce.php` in your theme’s 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. It’s also the method we use to integrate nicely with WordPress default themes.
|
||||
|
||||
Insert a few lines in your theme’s `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 you’re unsure of which classes or IDs to use, take a look at your theme’s `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 theme’s `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 it’s 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 – it’s 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 you’ll 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 theme’s `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 you’re 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).
|
|
@ -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 customer’s 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.';
|
||||
}
|
||||
|
||||
}
|
||||
```
|
|
@ -1,4 +1,4 @@
|
|||
# Fixing Outdated WooCommerce Templates
|
||||
# Fixing outdated WooCommerce templates
|
||||
|
||||
## Template Updates and Changes
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# User Experience Guidelines: Accessibility
|
||||
# User experience guidelines: accessibility
|
||||
|
||||
## Accessibility
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# User Experience Guidelines: Best Practices
|
||||
# User experience guidelines: best practices
|
||||
|
||||
## Best practices
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# User Experience Guidelines: Colors
|
||||
# User experience guidelines: colors
|
||||
|
||||
## Colors
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# User Experience Guidelines: Notices
|
||||
# User experience guidelines: notices
|
||||
|
||||
## Notices
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# User Experience Guidelines: Onboarding
|
||||
# User experience guidelines: onboarding
|
||||
|
||||
## Onboarding
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# User Experience Guidelines: Task list and Inbox
|
||||
# User experience guidelines: task list and inbox
|
||||
|
||||
## Task List & Inbox
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
# Configuring Caching Plugins for WooCommerce
|
||||
|
||||
## Excluding Pages from the Cache
|
||||
|
||||
Oftentimes if using caching plugins they’ll 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 you’re 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. |
|
||||
|
||||
We’re 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 plugin’s 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 doesn’t 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 don’t prevent my-account.php from being cached.
|
||||
|
||||
If you’re 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.
|
|
@ -1,4 +1,4 @@
|
|||
# WC CLI: Commands
|
||||
# WC CLI: commands
|
||||
|
||||
## wc shop_coupon
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Just changing package.json command for lint
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Create @woocommerce/admin-layout package to house header, footer, and similar components and utilities.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding support for modifying fill name to WooHeaderItem.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Make eslint emit JSON report for annotating PRs.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Upgrade TypeScript to 5.1.6
|
|
@ -1,5 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Applied lint auto fixes across monorepo
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: TypeScript build change
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Configuration change only
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Add missing dev dependency - rimraf
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding LayoutContext component and hook.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Update webpack config to use @woocommerce/internal-style-build's parser config
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add className to the MenuItem component
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix parseNumber to allow for emptry string thousand & decimal separators.
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add product list block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add ordering support to the product list
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add empty state when no attributes #41679
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
[Product Block Editor]: fix Input control issue in Manage download limit form
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
[Product Block Editor]: remove unused block attributes
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -18,7 +18,6 @@ $fixed-section-height: 224px;
|
|||
}
|
||||
|
||||
&__table {
|
||||
height: $fixed-section-height;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
flex: 1 0 auto;
|
||||
|
|
|
@ -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" ]
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './glasses';
|
||||
export * from './pants';
|
||||
export * from './shirt';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
} );
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
export interface UploadsBlockAttributes extends BlockAttributes {
|
||||
property: string;
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './add-products-modal';
|
||||
export * from './types';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
export type AddProductsModalProps = {
|
||||
initialValue: Product[];
|
||||
onSubmit( value: Product[] ): void;
|
||||
onClose(): void;
|
||||
};
|
|
@ -7,6 +7,9 @@
|
|||
&__add-new-icon {
|
||||
margin-right: $gap-small;
|
||||
}
|
||||
&__no-results {
|
||||
padding: $gap-small;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-experimental-select-control__popover-menu-container {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -51,3 +51,8 @@ export {
|
|||
TextControl as __experimentalTextControl,
|
||||
TextControlProps,
|
||||
} from './text-control';
|
||||
|
||||
export {
|
||||
AddProductsModal as __experimentalAddProductsModal,
|
||||
AddProductsModalProps,
|
||||
} from './add-products-modal';
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './use-draggable';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export type DraggableProps< T > = {
|
||||
onSort( fnState: ( items: T[] ) => T[] ): void;
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -52,9 +52,7 @@ export const Products = () => {
|
|||
} );
|
||||
|
||||
const { productTypes: productTypeListItems } = useProductTypeListItems(
|
||||
getProductTypes( {
|
||||
exclude: [ 'subscription' ],
|
||||
} ),
|
||||
getProductTypes(),
|
||||
[],
|
||||
{
|
||||
onClick: recordCompletionTime,
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue