diff --git a/.github/ISSUE_TEMPLATE/request-for-new-document.md b/.github/ISSUE_TEMPLATE/request-for-new-document.md new file mode 100644 index 00000000000..a1852d1f22d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request-for-new-document.md @@ -0,0 +1,30 @@ +--- +name: "\U0001F4C4 Request for New Document" +about: Suggest the creation of a new documentation topic that doesn't exist yet. +title: "[DOC-REQ]" +labels: 'type: documentation' +assignees: '' + +--- + +## Description of the Document Requested + +> Provide a detailed description of the topic you'd like to see documented. + + + +## Why is this Document Important? + +> Explain why this topic is crucial and how it can benefit the WooCommerce community. + + + +## Potential Content + +> If you have an idea about what the new document should cover, list the points or sub-topics here. + + + +## Additional Context + +> Add any other context, references, or information that can help in the creation of this new document. diff --git a/.github/ISSUE_TEMPLATE/suggestion-for-documentation-improvement-correction.md b/.github/ISSUE_TEMPLATE/suggestion-for-documentation-improvement-correction.md new file mode 100644 index 00000000000..0ab39efdb21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggestion-for-documentation-improvement-correction.md @@ -0,0 +1,30 @@ +--- +name: "\U0001F4DD Suggestion for Documentation Improvement/Correction" +about: Propose a specific improvement or correction for an existing document. +title: "[DOC-BUG]" +labels: 'type: documentation' +assignees: '' + +--- + +## Link to the Page/Section + +> Provide a link to the specific page or section that you're referring to. + + + +## Description of the Suggestion + +> Describe the changes you suggest to improve or correct the documentation. Be specific about any errors or areas of confusion you've identified. + + + +## Reason for the Suggestion + +> Why do you believe this change will make the documentation clearer or more accurate? + + + +## Additional Context + +> Add any other context, references, or screenshots that support your suggestion. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb30322f7fd..3db2b93bc05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ permissions: {} jobs: test: - name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} + name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }} timeout-minutes: 30 runs-on: ubuntu-20.04 permissions: diff --git a/.github/workflows/pr-unit-tests.yml b/.github/workflows/pr-unit-tests.yml index f1f8502e306..31b9d88e02c 100644 --- a/.github/workflows/pr-unit-tests.yml +++ b/.github/workflows/pr-unit-tests.yml @@ -15,7 +15,7 @@ permissions: {} jobs: test: if: ${{ github.event.pull_request.user.login != 'github-actions[bot]' }} - name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }} + name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }} ${{ matrix.hpos && 'HPOS' || '' }} timeout-minutes: 30 runs-on: ubuntu-20.04 permissions: diff --git a/docs/README.md b/docs/README.md index aa611d1d17d..ad515fa6cde 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,59 +1,90 @@ -# WooCommerce internal documentation +# WooCommerce Developer Documentation -This directory contains documentation about implementation details specific parts of the WooCommerce code base. This documentation is intended for developers. +> ⚠️ **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! -## Available documents +This is your go-to place to find everything you need to know to get started with WooCommerce development, including implementation details for specific parts of the WooCommerce code base. -* [HPOS](HPOS.md): Details of how the High Performance Order Storage works. +## Getting started -## Other documents +WooCommerce is a customizable, open-source eCommerce platform built on WordPress. It empowers businesses worldwide to sell anything from physical products and digital downloads to subscriptions, content, and even appointments. + +Get familiar with [WordPress Plugin Development](https://developer.wordpress.org/plugins/). + +Take a moment to familiarize yourself with our [Developer Resources](https://developer.wordpress.org/plugins/plugin-basics/). + +Once you're ready to move forward, consider one of the following: + +- [Tools for low code development](getting-started/tools-for-low-code-development.md) +- [Building your first extension](extension-development/building-your-first-extension.md) +- [How to design a simple extension](extension-development/how-to-design-a-simple-extension.md) + +## Contributions + +The WooCommerce ecosystem thrives on community contributions. Whether it's improving documentation, reporting bugs, or contributing code, we greatly appreciate every contribution from our community. + +- To contribute to **the core WooCommerce project**, check out our [Contributing guide](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md). +- To contribute to **documentation** please refer to the [documentation style guide](style-guide.md). + +## Support + +- To request a **new document, correction, or improvement**, [create an issue](https://github.com/woocommerce/woodocs/issues/new/choose). +- For development help, start with the [WooCommerce Community Forum](https://wordpress.org/support/plugin/woocommerce/), to see if someone else has already asked the same question. You can also pose your question in the `#developers` channel of our [Community Slack](https://woocommerce.com/community-slack/). If you're not sure where to ask your question, you can always [contact us](https://woocommerce.com/contact-us/), and our Happiness Engineers will be glad to point you in the right direction. +- For additional support with customizations, you might consider hiring from [WooExperts](https://woocommerce.com/experts/) or [Codeable](https://codeable.io/). + +### Additional Resources + +- [WooCommerce Official Website](https://woocommerce.com/) +- [Woo Marketplace](https://woocommerce.com/marketplace) +- All [WooCommerce Repositories on GitHub](https://woocommerce.github.io/) + +### Other documentation Some directories contain documentation about their own contents, in the form of README file. The available files are listed below, **if you create a new README file please add it to the corresponding list.** Available READMe files for the WooCommerce plugin: -* [`Root README`](../plugins/woocommerce/README.md) -* [`i18n/languages`](../plugins/woocommerce/i18n/languages/README.md) -* [`includes`](../plugins/woocommerce/includes/README.md) -* [`lib`](../plugins/woocommerce/lib/README.md) -* [`packages`](../plugins/woocommerce/packages/README.md) -* [`src`](../plugins/woocommerce/src/README.md) -* [`src/Admin/RemoteInboxNotifications`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md) -* [`src/Admin/RemoteInboxNotifications/Transformers`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md) -* [`src/Blocks`](../plugins/woocommerce/src/Blocks/README.md) -* [`src/Internal`](../plugins/woocommerce/src/Internal/README.md) -* [`src/Internal/Admin/ProductForm`](../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md) -* [`tests`](../plugins/woocommerce/tests/README.md) -* [`tests/api-core-tests`](../plugins/woocommerce/tests/api-core-tests/README.md) -* [`tests/e2e`](../plugins/woocommerce/tests/e2e/README.md) -* [`tests/e2e-pw`](../plugins/woocommerce/tests/e2e-pw/README.md) -* [`tests/performance`](../plugins/woocommerce/tests/performance/README.md) -* [`tests/Tools/CodeHacking`](../plugins/woocommerce/tests/Tools/CodeHacking/README.md) +- [`Root README`](../plugins/woocommerce/README.md) +- [`i18n/languages`](../plugins/woocommerce/i18n/languages/README.md) +- [`includes`](../plugins/woocommerce/includes/README.md) +- [`lib`](../plugins/woocommerce/lib/README.md) +- [`packages`](../plugins/woocommerce/packages/README.md) +- [`src`](../plugins/woocommerce/src/README.md) +- [`src/Admin/RemoteInboxNotifications`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md) +- [`src/Admin/RemoteInboxNotifications/Transformers`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md) +- [`src/Blocks`](../plugins/woocommerce/src/Blocks/README.md) +- [`src/Internal`](../plugins/woocommerce/src/Internal/README.md) +- [`src/Internal/Admin/ProductForm`](../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md) +- [`tests`](../plugins/woocommerce/tests/README.md) +- [`tests/api-core-tests`](../plugins/woocommerce/tests/api-core-tests/README.md) +- [`tests/e2e`](../plugins/woocommerce/tests/e2e/README.md) +- [`tests/e2e-pw`](../plugins/woocommerce/tests/e2e-pw/README.md) +- [`tests/performance`](../plugins/woocommerce/tests/performance/README.md) +- [`tests/Tools/CodeHacking`](../plugins/woocommerce/tests/Tools/CodeHacking/README.md) Available READMe files for the WooCommerce Admin plugin: -* [`Root README`](../plugins/woocommerce-admin/README.md) -* [`client/activity-panel`](../plugins/woocommerce-admin/client/activity-panel/README.md) -* [`client/activity-panel/activity-card`](../plugins/woocommerce-admin/client/activity-panel/activity-card/README.md) -* [`client/activity-panel/activity-header`](../plugins/woocommerce-admin/client/activity-panel/activity-header/README.md) -* [`client/analytics/report`](../plugins/woocommerce-admin/client/analytics/report/README.md) -* [`client/analytics/settings`](../plugins/woocommerce-admin/client/analytics/settings/README.md) -* [`client/dashboard`](../plugins/woocommerce-admin/client/dashboard/README.md) -* [`client/header`](../plugins/woocommerce-admin/client/header/README.md) -* [`client/marketing`](../plugins/woocommerce-admin/client/marketing/README.md) -* [`client/marketing/components/button`](../plugins/woocommerce-admin/client/marketing/components/button/README.md) -* [`client/marketing/components/card`](../plugins/woocommerce-admin/client/marketing/components/card/README.md) -* [`client/marketing/components/product-icon`](../plugins/woocommerce-admin/client/marketing/components/product-icon/README.md) -* [`client/utils`](../plugins/woocommerce-admin/client/utils/README.md) -* [`client/wp-admin-scripts`](../plugins/woocommerce-admin/client/wp-admin-scripts/README.md) -* [`docs`](../plugins/woocommerce-admin/docs/README.md) -* [`docs/examples`](../plugins/woocommerce-admin/docs/examples/README.md) -* [`docs/examples/extensions`](../plugins/woocommerce-admin/docs/examples/extensions/README.md) -* [`docs/features`](../plugins/woocommerce-admin/docs/features/README.md) -* [`docs/woocommerce.com`](../plugins/woocommerce-admin/docs/woocommerce.com/README.md) +- [`Root README`](../plugins/woocommerce-admin/README.md) +- [`client/activity-panel`](../plugins/woocommerce-admin/client/activity-panel/README.md) +- [`client/activity-panel/activity-card`](../plugins/woocommerce-admin/client/activity-panel/activity-card/README.md) +- [`client/activity-panel/activity-header`](../plugins/woocommerce-admin/client/activity-panel/activity-header/README.md) +- [`client/analytics/report`](../plugins/woocommerce-admin/client/analytics/report/README.md) +- [`client/analytics/settings`](../plugins/woocommerce-admin/client/analytics/settings/README.md) +- [`client/dashboard`](../plugins/woocommerce-admin/client/dashboard/README.md) +- [`client/header`](../plugins/woocommerce-admin/client/header/README.md) +- [`client/marketing`](../plugins/woocommerce-admin/client/marketing/README.md) +- [`client/marketing/components/button`](../plugins/woocommerce-admin/client/marketing/components/button/README.md) +- [`client/marketing/components/card`](../plugins/woocommerce-admin/client/marketing/components/card/README.md) +- [`client/marketing/components/product-icon`](../plugins/woocommerce-admin/client/marketing/components/product-icon/README.md) +- [`client/utils`](../plugins/woocommerce-admin/client/utils/README.md) +- [`client/wp-admin-scripts`](../plugins/woocommerce-admin/client/wp-admin-scripts/README.md) +- [`docs`](../plugins/woocommerce-admin/docs/README.md) +- [`docs/examples`](../plugins/woocommerce-admin/docs/examples/README.md) +- [`docs/examples/extensions`](../plugins/woocommerce-admin/docs/examples/extensions/README.md) +- [`docs/features`](../plugins/woocommerce-admin/docs/features/README.md) +- [`docs/woocommerce.com`](../plugins/woocommerce-admin/docs/woocommerce.com/README.md) Available READMe files for the WooCommerce Beta Tested plugin: -* [`Root README`](../plugins/woocommerce-beta-tester/README.md) -* [`src/tools`](../plugins/woocommerce-beta-tester/src/tools/README.md) -* [`userscripts`](../plugins/woocommerce-beta-tester/userscripts/README.md) +- [`Root README`](../plugins/woocommerce-beta-tester/README.md) +- [`src/tools`](../plugins/woocommerce-beta-tester/src/tools/README.md) +- [`userscripts`](../plugins/woocommerce-beta-tester/userscripts/README.md) diff --git a/docs/extension-development/building-your-first-extension.md b/docs/extension-development/building-your-first-extension.md new file mode 100644 index 00000000000..3a0177118f2 --- /dev/null +++ b/docs/extension-development/building-your-first-extension.md @@ -0,0 +1,117 @@ +# Building your first extension + +The easiest way to get started building an extension is to use the built-in extension generator that is included alongside WooCommerce Admin. This utility is maintained as part of the codebase for WooCommerce Admin, so it includes up-to-date tools and many preconfigured settings for building modern extensions that take advantage of the [React-powered](https://react.dev/) user experience available in current versions of WordPress and WooCommerce. + +## Using the extension generator + +Browse to your local WooCommerce Admin repository + +```sh +cd /your/server/wp-content/plugins/woocommerce-admin +``` + +Run the extension generator command + +```sh +npm run create-wc-extension +``` + +The extension generator will scaffold out a basic extension and place it in its own plugin directory alongside WooCommerce on your local server. + +The extension that the generator creates contains a simple [boilerplate](https://stackoverflow.com/questions/3992199/what-is-boilerplate-code) that handles much of the configuration needed for setting up a React-powered extension, which you can modify to fit your needs. + +## The architecture of a basic WooCommerce extension + +WooCommerce extensions use a combination of PHP and modern JavaScript to create a seamless user experience for merchants and shoppers that takes advantage of the features and functionality available in the [NodeJS](https://nodejs.org/en) ecosystem while still being a good neighbor within the underlying WordPress application environment. + +WordPress plugins (of which WooCommerce extensions are a specialized subset), tend to follow a few common patterns. You can read more about common WordPress plugin architecture in the [Best Practices chapter of the WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#architecture-patterns). + +In addition to the main PHP file that all WordPress plugins must contain, a WooCommerce extension will typically contain additional PHP files with classes that assist in server-side functionality. + +It will also contain files that are JavaScript and CSS assets which shape the client-side behavior and appearance. + +## File structure generated by the `create-wc-extension script` + +When you run the built-in extension generator, it will output something that looks similar to the structure below. + +```sh +. +├── README.md +├── my-great-extension.php +├── package.json +├── src +│ ├── index.js +│ └── index.scss +└── webpack.config.js +``` + +Here’s a breakdown of what these files are and what purpose they serve: + +`README.md` +This file is meant to have a high-level overview of your extension to make it easier for people to use and extend your project. The generator outputs a basic file with some minimal instructions in it to get you started, but you should replace the contents of the file with information specific to your project. It’s important to keep in mind that this file is not the same as the readme.txt file required by WordPress.org plugin directory, which must adhere to specific file standads. + +`[your-extension-name].php` +This is your extension’s main PHP file. It functions as the entry point for your extension and is where you’ll likely include code that hooks your extension into WordPress and WooCommerce. You can read more about the purpose of this file in the Getting Started section of the WordPress Plugin Developer Handbook. + +`package.json` +This is a manifest file that Node uses for a number of different purposes. It can store configuration settings for tools, lists of dependencies, aliases for common scripts, and even metadata about your extension. The WooCommerce extension generator outputs a package.json file that will bundle many helpful dependencies with your extension, as well as a variety of scripts you can use in conjunction with these dependencies to streamline your workflow and make sure your extension conforms to the same standards as other WordPress plugins and WooCommerce extensions. Here’s an example of what your package.json file might look like initially: + +```json +{ + "name": "my-great-extension", + "title": "my-great-extension", + "license": "GPL-3.0-or-later", + "version": "0.1.0", + "description": "my-great-extension", + "scripts": { + "build": "wp-scripts build", + "check-engines": "wp-scripts check-engines", + "check-licenses": "wp-scripts check-licenses", + "format:js": "wp-scripts format-js", + "lint:css": "wp-scripts lint-style", + "lint:js": "wp-scripts lint-js", + "lint:md:docs": "wp-scripts lint-md-docs", + "lint:md:js": "wp-scripts lint-md-js", + "lint:pkg-json": "wp-scripts lint-pkg-json", + "packages-update": "wp-scripts packages-update", + "start": "wp-scripts start", + "test:e2e": "wp-scripts test-e2e", + "test:unit": "wp-scripts test-unit-js" + }, + "devDependencies": { + "@wordpress/scripts": "^12.2.1", + "@woocommerce/eslint-plugin": "1.1.0", + "@woocommerce/dependency-extraction-webpack-plugin": "1.1.0" + } +} +``` + +The settings in this autogenerated file tell Webpack to use the default configuration included with the `@wordpress/scripts` package (listed in your `package.json` as a development dependency) and to override the plugin it uses for dependency extraction with one that is tailor-made for WooCommerce extensions. + +## Try out your extension + +If you used the extension generator to create your extension, you’ll need to complete a few final steps to see it in action. + +First, navigate to your extension’s root directory on your development server: + +```sh +cd /your/server/wc-content/plugins/your-extension/ +``` + +Then install the project’s dependencies. + +```sh +npm install +``` + +Finally, run the start script to generate an initial build of your extension. This script will also continuously watch your local files for changes. + +```sh +npm start +``` + +Once your initial build is complete, you can browse to the administrative area of your local WordPress environment and activate your extension. If everything worked as it should, you should see a message in your browser’s JavaScript console: + +```sh +hello world +``` diff --git a/docs/extension-development/development-environment.md b/docs/extension-development/development-environment.md new file mode 100644 index 00000000000..822eb0dcf65 --- /dev/null +++ b/docs/extension-development/development-environment.md @@ -0,0 +1,205 @@ +# Setting up your development environment + +## Introduction + +Building an extension for WooCommerce is a straightforward process, but there are a several moving parts and a few supporting software tools you’ll want to familiarize yourself with. This guide will walk you through the steps of getting a basic development environment set up for building WooCommerce extensions. + +If you would like to contribute to the WooCommerce core platform; please read our [contributor documentation and guidelines](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment). + +## Prerequisites + +### Recommended reading + +WooCommerce extensions are a specialized type of WordPress plugin. If you are new to WordPress plugin development, take a look at a few of these articles in the [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/). + +### Required software + +[Git](https://git-scm.com/) +[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) +[NodeJS](https://nodejs.org/en) +[PNpm](https://pnpm.io/) +[Composer](https://getcomposer.org/download/) + +Note: If you’re working on a Windows machine, you may want to take a look at the Building Extensions in Windows Environments section of this guide before proceeding. + +### Setting up your reusable WordPress development environment + +In addition to the software listed above, you’ll also want to have some way of setting up a local development server stack. There are a number of different tools available for this, each with a certain set of functionality and limitations. We recommend choosing an option below that fits your preferred workflow best. + +### WordPress-specific tools + +[vvv](https://varyingvagrantvagrants.org/) – A highly configurable, cross-platform, and robust environment management tool powered by VirtualBox and Vagrant. This is one the tool that the WooCommerce Core team recommends to contributors. + +[wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) – A command-line utility maintained by the WordPress community that allows you to set up and run custom WordPress environments with Docker and JSON manifests. + +[LocalWP](https://localwp.com/) – A cross-platform app that bills itself as a one-click WordPress installation. + +### General PHP-based web stack tools + +[MAMP](https://www.mamp.info/en/mac/) – A local server environment that can be installed on Mac or Windows. + +[WAMP](https://www.wampserver.com/en/) – A Windows web development environment that lets you create applications with Apache2, PHP, and MySQL. + +[XAMPP](https://www.apachefriends.org/index.html) – An easy-to-install Apache distribution containing MariaDB, PHP, and Perl. It’s available for Windows, Linux, and OS X. + +### Minimum server requirements + +Regardless of the tool you choose for managing your development environment, you should make sure it [meets the server recommendations](https://woocommerce.com/document/server-requirements/?utm_source=wooextdevguide) for WooCommerce as well as the [requirements for running WordPress](https://wordpress.org/about/requirements/). + +## Anatomy of a WordPress development environment (public_html/) + +While development environments can vary, the basic file structure for a WordPress environment should be consistent. + +When developing a WooCommerce extension, you’ll usually be doing most of your work within the public_html directory of your local server. For now, take some time to familiarize yourself with a few key paths: + +`wp-content/debug.log` – This is the file where WordPress writes the important output such as errors and other messages useful for debugging. + +`wp-content/plugins/` – This is the directory on the server where WordPress plugin folders live. + +`wp-content/themes/` – This is the directory on the server where WordPress theme folders live. + +## Adding WooCommerce Core to your environment + +When developing an extension for WooCommerce, it’s helpful to install a development version of WooCommerce core. + +### Clone the WC Core repo into `wp-content/plugins/` + +```sh +cd /your/server/wp-content/plugins +git clone https://github.com/woocommerce/woocommerce.git +cd woocommerce +``` + +### Activate the required Node version + +```sh +nvm use + +Found '/path/to/woocommerce/.nvmrc' with version +Now using node v12.21.0 (npm v6.14.11) +``` + +Note: if you don’t have the required version of Node installed, NVM will alert you so you can install it: + +```sh +Found '/path/to/woocommerce/.nvmrc' with version +N/A: version "v12 -> N/A" is not yet installed. + +You need to run "nvm install v12" to install it before using it. +``` + +### Install dependencies + +```sh +pnpm install && composer install +``` + +### Build WooCommerce + +```sh +pnpm run build +``` + +Running this script will compile the JavaScript and CSS that WooCommerce needs to operate. If you try to run WooCommerce on your server without generating the compiled assets, you may experience errors and other unwanted side-effects. + +Note: In some environments, you may see an out-of-memory error when you try to build WooCommerce. If this happens, you simply need to adjust the memory_limit setting in your environment’s php.ini configuration to a higher value. The process for changing this value varies depending on the environment management tooling you use, so it’s best to consult your tool’s documentation before making any changes. + +## Adding WooCommerce Admin to your environment + +Installing a development version of WooCommerce Admin will give you access to some helpful utilities such as a built-in script for generating React-powered WooCommerce extensions. + +### Clone the WC Admin repo into `wp-content/plugins/` + +```sh +cd /your/server/wp-content/plugins +git clone https://github.com/woocommerce/woocommerce-admin.git +cd woocommerce-admin +``` + +### Activate the required Node version + +```sh +nvm use +Found '/path/to/woocommerce-admin/.nvmrc' with version +Now using node v14.16.0 (npm v6.14.11) +``` + +Note: if you don’t have the required version of Node installed, NVM will alert you so you can install it. + +```sh +Found '/path/to/woocommerce-admin/.nvmrc' with version +N/A: version "lts/* -> N/A" is not yet installed. + +You need to run "nvm install lts/*" to install it before using it. +``` + +Pro-tip: WooCommerce Admin may require a different version of Node than WooCommerce Core requires. Keep this in mind when navigating between directories using the same shell session. As a best practice, always make sure to activate the correct version of Node using nvm use before running any commands inside a cloned repository. + +### Install dependencies + +```sh +npm install && composer install +``` + +## Build a development version of WooCommerce Admin + +Building a development version will compile unminified versions of asset files, which is useful when debugging extensions that interact with WooCommerce Admin features. + +```sh +npm run dev +``` + +If you run into trouble when building WooCommerce Admin, take a look at this wiki article for troubleshooting help. + +## Adding WooCommerce Blocks to your environment + +Installing a development version of WooCommerce Blocks is not required in every case, but having a standalone installation of the feature-plugin version of this extension allows you to work with the latest features, which can be helpful for compatibility testing and future-proofing your extension. + +### Clone the WC Blocks repo into `wp-content/plugins/` + +```sh +cd /your/server/wp-content/plugins +git clone https://github.com/woocommerce/woocommerce-gutenberg-products-block.git +cd woocommerce-gutenberg-products-block +``` + +### Activate the required Node version + +```sh +nvm use + +Found '/path/to/woocommerce-gutenberg-products-block/.nvmrc' with version +Now using node v14.16.0 (npm v6.14.11) +``` + +Note: if you don’t have the required version of Node installed, NVM will alert you so you can install it. + +```sh +Found '/path/to/woocommerce-gutenberg-products-block/.nvmrc' with version +N/A: version "lts/* -> N/A" is not yet installed. + +You need to run "nvm install lts/*" to install it before using it. +``` + +Pro-tip: WooCommerce Blocks may require a different version of Node than WooCommerce Core requires. Keep this in mind when navigating between directories using the same shell session. As a best practice, always make sure to activate the correct version of Node using nvm use before running any commands inside a cloned repository. + +### Install dependencies + +```sh +npm install && composer install +Build the assets +npm run build +``` + +This will compile and minify the JavaScript and CSS from the /assets directory to be served. + +## Finishing up + +Once you have WooCommerce and its sibling extensions installed in your WordPress environment, start up your server, browse to your site and handle any initial setup steps or importing you’d like to do. This is a good time to load sample data and activate themes and plugins. + +Depending on which extensions you installed in your environment you should have one or more of the following directories in your `public_html` directory: + +- `wp-content/plugins/woocommerce` +- `wp-content/plugins/woocommerce-admin` +- `wp-content/plugins/woocommerce-gutenberg-products-block` +- `wp-content/themes/storefront` diff --git a/docs/extension-development/extension-developer-handbook.md b/docs/extension-development/extension-developer-handbook.md new file mode 100644 index 00000000000..5b7c22f3cff --- /dev/null +++ b/docs/extension-development/extension-developer-handbook.md @@ -0,0 +1,231 @@ +# 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://www.google.com/url?q=https://developer.wordpress.org/plugins/&sa=D&source=editors&ust=1692724061394513&usg=AOvVaw1PmatucFlJ3lI0z15KYBFq). + +Your WooCommerce extension should: + +- Adhere to all WordPress plugin coding standards, as well as [best practice guidelines](https://www.google.com/url?q=https://developer.wordpress.org/plugins/plugin-basics/best-practices/&sa=D&source=editors&ust=1692724061394795&usg=AOvVaw1vZcSq6JuW0VNm3HhUSb9s) for harmonious existence within WordPress and alongside other WordPress plugins. +- Have a single core purpose and use WooCommerce features as much as possible. +- Not do anything malicious, illegal, or dishonest — for example, inserting spam links or executable code via third-party systems if not part of the service or  explicitly permitted in the service’s terms of use. +- Adhere to WooCommerce [compatibility and interoperability guidelines](https://www.google.com/url?q=https://woocommerce.com/document/marketplace-overview/%23section-9&sa=D&source=editors&ust=1692724061395243&usg=AOvVaw2qsdAnXBb2o2dmrTg_QKaa). + +Merchants make use of WooCommerce extensions daily, and should have a unified and pleasant experience while doing so without advertising invading their WP Admin or store. + +Note: We provide this page as a best practice for developers. + +## [Check if WooCommerce is active](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-1&sa=D&source=editors&ust=1692724061395542&usg=AOvVaw2bTUi1Q7fivFhe-Gc3VULl) + +Most WooCommerce plugins do not need to run unless WooCommerce is already active. You can wrap your plugin in a check to see if WooCommerce is installed: + +``` +// Test to see if WooCommerce is active (including network activated). + +$plugin_path = trailingslashit( WP_PLUGIN_DIR ) . 'woocommerce/woocommerce.php'; + +if ( + +in_array( $plugin_path, wp_get_active_and_valid_plugins() ) + +|| in_array( $plugin_path, wp_get_active_network_plugins() ) + +) { + +// Custom code here. WooCommerce is active, however it has not + +// necessarily initialized (when that is important, consider + +// using the \`woocommerce_init\` action). + +} +``` + +Note that this check will fail if the WC plugin folder is named anything other than woocommerce. + +## [Main file naming](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-2&sa=D&source=editors&ust=1692724061396656&usg=AOvVaw0bg5CY1zbmVRBUpvcbFoWc) + +The main plugin file should adopt the name of the plugin, e.g., A plugin with the directory name plugin-name would have its main file named plugin-name.php. + +## [Text domains](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-3&sa=D&source=editors&ust=1692724061397135&usg=AOvVaw2mG8ZyvrV7HLq35afWjwcw) + +Follow guidelines for [Internationalization for WordPress Developers](https://www.google.com/url?q=https://codex.wordpress.org/I18n_for_WordPress_Developers&sa=D&source=editors&ust=1692724061397498&usg=AOvVaw3sWMtUFCwi2CM4BnCL9T3w), the text domain should match your plugin directory name, e.g., A plugin with a directory name of plugin-name would have the text domain plugin-name. Do not use underscores. + +## [Localization](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-4&sa=D&source=editors&ust=1692724061397875&usg=AOvVaw0aN3CAxkWHDXAaOEAt_XLg) + +All text strings within the plugin code should be in English. This is the WordPress default locale, and English should always be the first language. If your plugin is intended for a specific market (e.g., Spain or Italy), include appropriate translation files for those languages within your plugin package. Learn more at [Using Makepot to translate your plugin](https://www.google.com/url?q=https://codex.wordpress.org/I18n_for_WordPress_Developers%23Translating_Plugins_and_Themes&sa=D&source=editors&ust=1692724061398312&usg=AOvVaw1KI1tPNBz1PhXghD6EPeFX). + +## [Follow WordPress PHP Guidelines](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-5&sa=D&source=editors&ust=1692724061398556&usg=AOvVaw3r1rBtiLQqnd09_uqcbgN1) + +WordPress has a [set of guidelines](https://www.google.com/url?q=http://make.wordpress.org/core/handbook/coding-standards/php/&sa=D&source=editors&ust=1692724061398880&usg=AOvVaw32UFCkh2lVnQ1P11WK5917) to keep all WordPress code consistent and easy to read. This includes quotes, indentation, brace style, shorthand php tags, yoda conditions, naming conventions, and more. Please review the guidelines. + +Code conventions also prevent basic mistakes, as [Apple made with iOS 7.0.6](https://www.google.com/url?q=https://www.imperialviolet.org/2014/02/22/applebug.html&sa=D&source=editors&ust=1692724061399164&usg=AOvVaw0U7fB5ITS8uXELL3MgR3zx). + +## [Custom Database Tables & Data Storage](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-6&sa=D&source=editors&ust=1692724061399464&usg=AOvVaw2baqakEqCZi76lbxB3zjh9) + +Avoid creating custom database tables. Whenever possible, use WordPress [post types](https://www.google.com/url?q=http://codex.wordpress.org/Post_Types%23Custom_Post_Types&sa=D&source=editors&ust=1692724061399803&usg=AOvVaw0flq0h728aDmJWR23oNv0V), [taxonomies](https://www.google.com/url?q=http://codex.wordpress.org/Taxonomies&sa=D&source=editors&ust=1692724061399949&usg=AOvVaw1qbvRfl8wcPI35lvSboCwi), and [options](https://www.google.com/url?q=http://codex.wordpress.org/Creating_Options_Pages&sa=D&source=editors&ust=1692724061400101&usg=AOvVaw3H8WjoRljUHd6q5s8X_Pdi). + +Consider the permanence of your data. Here’s a quick primer: + +- If the data may not always be present (i.e., it expires), use a transient. +- If the data is persistent but not always present, consider using the WP Cache. +- If the data is persistent and always present, consider the wp_options table. +- If the data type is an entity with n units, consider a post type. +- If the data is a means or sorting/categorizing an entity, consider a taxonomy. + +Logs should be written to a file using the [WC_Logger](https://www.google.com/url?q=https://woocommerce.com/wc-apidocs/class-WC_Logger.html&sa=D&source=editors&ust=1692724061401335&usg=AOvVaw3mxPgYSD7oL2sCoQNcN1BO) class. + +## [Prevent Data Leaks](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-7&sa=D&source=editors&ust=1692724061401572&usg=AOvVaw3xUKNB9qgJDqnd9RwlY8iT) + +Try to prevent direct access data leaks. Add this line of code after the opening PHP tag in each PHP file: + +``` +if ( ! defined( 'ABSPATH' ) ) { +exit; // Exit if accessed directly +} +``` + +## [Readme](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-8&sa=D&source=editors&ust=1692724061402226&usg=AOvVaw0phoD93bjkbxKs01VSxbm_) + +All plugins need a [standard WordPress readme](https://www.google.com/url?q=http://wordpress.org/plugins/about/readme.txt&sa=D&source=editors&ust=1692724061402537&usg=AOvVaw0CxV8gQGI6n0FztcJ_yxwr). + +Your readme might look something like this: + +``` +=== Plugin Name === +Contributors: (this should be a list of wordpress.org userid's) +Tags: comments, spam +Requires at least: 4.0.1 +Tested up to: 4.3 +Requires PHP: 5.6 +Stable tag: 4.3 +License: GPLv3 or later License +URI: http://www.gnu.org/licenses/gpl-3.0.html +``` + +## [Plugin Author Name](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-9&sa=D&source=editors&ust=1692724061403627&usg=AOvVaw0C49AD_KHbRbjBvuZif55T) + +To ensure a consistent experience for all WooCommerce users,including finding information on who to contact with queries, the following plugin headers should be in place: + +- The Plugin Author isYourName/YourCompany +- The Developer header is YourName/YourCompany, with the Developer URI field listed as http://yourdomain.com/ + +For example: + +``` +/** +* Plugin Name: WooCommerce Extension +* Plugin URI: http://woocommerce.com/products/woocommerce-extension/ +* Description: Your extension's description text. +* Version: 1.0.0 +* Author: Your Name +* Author URI: http://yourdomain.com/ +* Developer: Your Name +* Developer URI: http://yourdomain.com/ +* Text Domain: woocommerce-extension +* Domain Path: /languages +* +* Woo: 12345:342928dfsfhsf8429842374wdf4234sfd +* WC requires at least: 2.2 +* WC tested up to: 2.3 +* +* License: GNU General Public License v3.0 +* License URI: http://www.gnu.org/licenses/gpl-3.0.html +*/ +``` + +## [Declaring required and supported WooCommerce version](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-10&sa=D&source=editors&ust=1692724061406115&usg=AOvVaw17Ag30ypAdPnc0BXtUdyUo) + +Use the follow headers to declare “required” and “tested up to” versions: + +- WC requires at least +- WC tested up to + +## [Plugin URI](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-11&sa=D&source=editors&ust=1692724061406678&usg=AOvVaw2A80jh9ZfkI6nLGIa93Hpm) + +Ensure that the Plugin URI line of the above plugin header is provided. This line should contain the URL of the plugin’s product/sale page or to a dedicated page for the plugin on your website. + +## [Make it Extensible](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-13&sa=D&source=editors&ust=1692724061407164&usg=AOvVaw3UxTnE6_-W2mM5rVyBk6BY) + +Developers should use WordPress actions and filters to allow for modification/customization without requiring users to touch the plugin’s core code base. + +If your plugin creates a front-end output, we recommend to having a templating engine in place so users can create custom template files in their theme’s WooCommerce folder to overwrite the plugin’s template files. + +For more information, check out Pippin’s post on [Writing Extensible Plugins with Actions and Filters](https://www.google.com/url?q=http://code.tutsplus.com/tutorials/writing-extensible-plugins-with-actions-and-filters--wp-26759&sa=D&source=editors&ust=1692724061407755&usg=AOvVaw1RO30KUvw73kAb73j2Mjxs). + +## [Use of External Libraries](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-14&sa=D&source=editors&ust=1692724061408050&usg=AOvVaw064nKRX-btaU6-rP2nDvPR) + +The use of entire external libraries is typically not suggested as this can open up the product to security vulnerabilities. If an external library is absolutely necessary, developers should be thoughtful about the code used and assume ownership as well as of responsibility for it. Try to  only include the strictly necessary part of the library, or use a WordPress-friendly version or opt to build your own version. For example, if needing to use a text editor such as TinyMCE, we recommend using the WordPress-friendly version, TinyMCE Advanced. + +## [Remove Unused Code](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-15&sa=D&source=editors&ust=1692724061408520&usg=AOvVaw1xpjcmMrZLm46Jgpa_VJdb) + +With version control, there’s no reason to leave commented-out code; it’s annoying to scroll through and read. Remove it and add it back later if needed. + +## [Comment](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-16&sa=D&source=editors&ust=1692724061408902&usg=AOvVaw1ZMYYAAWMyPDO1S4YMKRLL) + +If you have a function, what does the function do? There should be comments for most if not all functions in your code. Someone/You may want to modify the plugin, and comments are helpful for that. We recommend using [PHP Doc Blocks](https://www.google.com/url?q=http://en.wikipedia.org/wiki/PHPDoc&sa=D&source=editors&ust=1692724061409214&usg=AOvVaw0pK1khHhpHhP1aU6Wfgg7l)  similar to [WooCommerce](https://www.google.com/url?q=https://github.com/woocommerce/woocommerce/&sa=D&source=editors&ust=1692724061409366&usg=AOvVaw3UOb2ML3qmjH-MUwEYxwZN). + +## [Avoid God Objects](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-17&sa=D&source=editors&ust=1692724061409596&usg=AOvVaw1jhRY0Ozls9pwiMi12lNkG) + +[God Objects](https://www.google.com/url?q=http://en.wikipedia.org/wiki/God_object&sa=D&source=editors&ust=1692724061409851&usg=AOvVaw0XP2zyCLmDVwGMwNj0XxF8) are objects that know or do too much. The point of object-oriented programming is to take a large problem and break it into smaller parts. When functions do too much, it’s hard to follow their logic, making bugs harder to fix. Instead of having massive functions, break them down into smaller pieces. + +## [Test Extension Quality & Security with Quality Insights Tool](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-18&sa=D&source=editors&ust=1692724061410124&usg=AOvVaw3WxIdaWb-loeDgk5idgYh7) + +Integrate the [Quality Insights Toolkit (QIT)](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/&sa=D&source=editors&ust=1692724061410435&usg=AOvVaw3-rM1B3-aofWDpJB5y-9Qs) into your development workflow to ensure your extension adheres to WordPress / WooCommerce quality and security standards. The QIT allows the ability to test your extensions against new releases of PHP, WooCommerce, and WordPress, as well as other active extensions, at the same time. The following tests are available today: + +- [End-to-End](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/e2e&sa=D&source=editors&ust=1692724061410720&usg=AOvVaw2-K7A2Jp9eEE3I7yg5GtLw) +- [Activation](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/activation&sa=D&source=editors&ust=1692724061410980&usg=AOvVaw3EGJl6KSaQL1ygcvoDFFvR) +- [Security](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/security&sa=D&source=editors&ust=1692724061411228&usg=AOvVaw3t5gYK8Md1UQWZORTmKSSx) +- [PHPStan](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/phpstan&sa=D&source=editors&ust=1692724061411473&usg=AOvVaw0cYoAE7ScAXqMU7aw5gAOB) +- [API](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/api&sa=D&source=editors&ust=1692724061411713&usg=AOvVaw0dXv3dyfNaAwe6wiwqApHn) + +## [Test Your Code with WP_DEBUG](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-19&sa=D&source=editors&ust=1692724061411947&usg=AOvVaw3UsBUZeYvFu9v4itS839zy) + +Always develop with [WP_DEBUG](https://www.google.com/url?q=http://codex.wordpress.org/Debugging_in_WordPress&sa=D&source=editors&ust=1692724061412254&usg=AOvVaw1412x2vFGfPDqCXxohD-JF) mode on, so you can see all PHP warnings sent to the screen. This will flag things like making sure a variable is set before checking the value. + +## [Separate Business Logic & Presentation Logic](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-20&sa=D&source=editors&ust=1692724061412504&usg=AOvVaw1-plFhPYmTkkU99E9IO3VN) + +It’s a good practice to separate business logic (i.e., how the plugin works) from [presentation logic](https://www.google.com/url?q=http://en.wikipedia.org/wiki/Presentation_logic&sa=D&source=editors&ust=1692724061412782&usg=AOvVaw3graTlf6ciHm0E3N25NOMQ) (i.e., how it looks). Two separate pieces of logic are more easily maintained and swapped if necessary. An example is to have two different classes — one for displaying the end results, and one for the admin settings page. + +## [Use Transients to Store Offsite Information](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-21&sa=D&source=editors&ust=1692724061413039&usg=AOvVaw0_S-ooFW3W6n-k4yV3Gbmo) + +If you provide a service via an API, it’s best to store that information so future queries can be done faster and the load on your service is lessened. [WordPress transients](https://www.google.com/url?q=http://codex.wordpress.org/Transients_API&sa=D&source=editors&ust=1692724061413333&usg=AOvVaw2SqfyKOl4wa52wmN_B0iJw) can be used to store data for a certain amount of time. + +## [Logging Data](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-22&sa=D&source=editors&ust=1692724061413569&usg=AOvVaw1Rz8wUNYXdGr4LnOCiOpQM) + +You may want to log data that can be useful for debugging purposes. This is great with two conditions: + +- Allow any logging as an ‘opt in’. +- Use the [WC_Logger](https://www.google.com/url?q=https://woocommerce.com/wc-apidocs/class-WC_Logger.html&sa=D&source=editors&ust=1692724061414103&usg=AOvVaw1Xl7lewASbQMGaV8Frgq-U) class. A user can then view logs on their system status page. + +If adding logging to your extension, here’s a snippet for presenting a link to the logs, in a way the extension user can easily make use of. + +``` +$label = \_\_( 'Enable Logging', 'your-textdomain-here' ); + +$description = \_\_( 'Enable the logging of errors.', 'your-textdomain-here' ); + +if ( defined( 'WC_LOG_DIR' ) ) { + +$log_url = add_query_arg( 'tab', 'logs', add_query_arg( 'page', 'wc-status', admin_url( 'admin.php' ) ) ); + +$log_key = 'your-plugin-slug-here-' . sanitize_file_name( wp_hash( 'your-plugin-slug-here' ) ) . '-log'; + +$log_url = add_query_arg( 'log_file', $log_key, $log_url ); + +$label .= ' | ' . sprintf( \_\_( '%1$sView Log%2$s', 'your-textdomain-here' ), '', '' ); + +} + +$form_fields\['wc_yourpluginslug_debug'\] = array( + +'title' => \_\_( 'Debug Log', 'your-textdomain-here' ), + +'label' => $label, + +'description' => $description, + +'type' => 'checkbox', + +'default' => 'no' + +); +``` \ No newline at end of file diff --git a/docs/extension-development/handling-deactivation-and-uninstallation.md b/docs/extension-development/handling-deactivation-and-uninstallation.md new file mode 100644 index 00000000000..4733ddf7b5f --- /dev/null +++ b/docs/extension-development/handling-deactivation-and-uninstallation.md @@ -0,0 +1,50 @@ +# Handling deactivation and uninstallation + +## Introduction + +There are a number of cleanup tasks you’ll need to handle when a merchant deactivates or uninstalls your extension. This guide provides a brief overview of WooCommerce-specific items you’ll want to make sure you account for when defining your extension’s deactivation and uninstallation logic. + +## Removing Scheduled Actions + +If your extension uses Action Scheduler to queue any background jobs, it’s important to unschedule those actions when your extension is uninstalled or deactivated. + +`as_unschedule_all_actions( $hook, $args, $group );` + +You can read more about using Action Scheduler for managing background processing in the [Action Scheduler API Reference](https://actionscheduler.org/api/). + +## Removing Admin Notes + +If you have created any Notes for merchants, you should delete those notes when your extension is deactivated or, at the very least, when it is uninstalled. + +```php +function my_great_extension_deactivate() { + ExampleNote::possibly_delete_note(); +} +register_deactivation_hook( __FILE__, 'my_great_extension_deactivate' ); + +``` + +The example above assumes that you have followed the pattern this guide recommends for creating Notes as dedicated classes that include the `NoteTraits` trait included with WooCommerce Admin. This approach provides your Note with some baked in functionality that streamlines note operations such as creation and deletion. + +## Removing Admin Tasks + +When your extension is deactivated or uninstalled, you should take care to unregister any tasks that your extension created for merchants. + +```php +// Unregister task. +function my_extension_deactivate_task() { + remove_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 ); +} + +register_deactivation_hook( __FILE__, 'my_extension_deactivate_task' ); +``` + +Keep in mind that merchant tasks are managed via a hybrid approach that involves both PHP and JavaScript, so the client-side registration only happens when your extension’s JavaScript runs. + +## Unregistering navigation + +When your extension deactivates and uninstalls, any registration you’ve done with the WooCommerce Navigation will be handled automatically. + +## WordPress cleanup tasks + +There are additional measures you may need to consider when your extension is deactivated or uninstalled, depending on the types of modifications it makes to the underlying WordPress environment when it activates and runs. You can read more about handling deactivation and uninstallation in the [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/intro/). diff --git a/docs/extension-development/handling-merchant-onboarding.md b/docs/extension-development/handling-merchant-onboarding.md new file mode 100644 index 00000000000..d2fb37168ff --- /dev/null +++ b/docs/extension-development/handling-merchant-onboarding.md @@ -0,0 +1,655 @@ +# Handling merchant onboarding + +## Introduction + +Onboarding is a critical part of the merchant’s user experience. It helps set them up for success and ensures they’re not only using your extension correctly but also getting the most out of it. There are a few especially useful features that you can take advantage of as a developer to help onboard merchants who are using your extension: + +- Setup tasks +- Store management links +- Admin notes + +--- + +## Using setup tasks + +Setup tasks appear on the WooCommerce Admin home screen and prompt a merchant to complete certain steps in order to set up your extension. Adding tasks is a two-step process that requires: + +- Registering the task (and its JavaScript) using PHP +- Using JavaScript to build the task, set its configuration, and add it to the task list + +### Registering the task with PHP + +To register your task as an extended task list item, you’ll need to hook in to the `woocommerce_get_registered_extended_tasks` filter with a function that appends your task to the array the filter provides. + +```php +// Task registration +function my_extension_register_the_task( $registered_tasks_list_items ) { + $new_task_name = 'your_task_name'; + if ( ! in_array( $new_task_name, $registered_tasks_list_items, true ) ) { + array_push( $registered_tasks_list_items, $new_task_name ); + } + return $registered_tasks_list_items; +} +add_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 ); +``` + +### Registering the task’s JavaScript + +In addition to registering the task name, you’ll also need to register and enqueue the transpiled JavaScript file containing your task component, its configuration, and its event-handlers. A common way to do this is to create a dedicated registration function that hooks into the `admin_enqueue_scripts` action in WordPress. If you do things this way, you can nest the `add_filter` call for `woocommerce_get_registered_extended_tasks` in this function as well. Below is an annotated example of how this registration might look: + +```php +// Register the task list item and the JS. +function add_task_register_script() { + + // Check to make sure that this is a request for an Admin page. + if ( + ! class_exists( 'Automattic\WooCommerce\Admin\Loader' ) || + ! \Automattic\WooCommerce\Admin\Loader::is_admin_page() || + ! Onboarding::should_show_tasks() + ) { + return; + } + + // Register a handle for your extension's transpiled JavaScript file. + wp_register_script( + 'add-task', + plugins_url( '/dist/index.js', __FILE__ ), + array( + 'wp-hooks', + 'wp-element', + 'wp-i18n', + 'wc-components', + ), + filemtime( dirname( __FILE__ ) . '/dist/index.js' ), + true + ); + + // Get server-side data via PHP and send it to the JavaScript using wp_localize_script + $client_data = array( + 'isComplete' => get_option( 'woocommerce_admin_add_task_example_complete', false ), + ); + wp_localize_script( 'add-task', 'addTaskData', $client_data ); + + // Enqueue the script in WordPress + wp_enqueue_script( 'add-task' ); + + // Hook your task registration script to the relevant extended tasks filter + add_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 ); +} +``` + +### Unregistering the task upon deactivation + +It is also helpful to define a function that will unregister your task when your extension is deactivated. + +```php +// Unregister task. +function my_extension_deactivate_task() { + remove_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 ); +} + +register_deactivation_hook( __FILE__, 'my_extension_deactivate_task' ); + +``` + +### Adding the task using JavaScript + +Once the task has been registered in WooCommerce, you need to build the task component, set its configuration, and add it to the task list. For example, the JavaScript file for a simple task might look something like this: + +```js +// External dependencies. +import { addFilter } from '@wordpress/hooks'; +import apiFetch from '@wordpress/api-fetch'; +import { Card, CardBody } from '@wordpress/components'; + +// WooCommerce dependencies. +import { getHistory, getNewPath } from '@woocommerce/navigation'; + +// Event handler for handling mouse clicks that mark a task complete. +const markTaskComplete = () => { + // Here we're using apiFetch to set option values in WooCommerce. + apiFetch( { + path: '/wc-admin/options', + method: 'POST', + data: { woocommerce_admin_add_task_example_complete: true }, + } ) + .then( () => { + // Set the local `isComplete` to `true` so that task appears complete on the list. + addTaskData.isComplete = true; + // Redirect back to the root WooCommerce Admin page. + getHistory().push( getNewPath( {}, '/', {} ) ); + } ) + .catch( ( error ) => { + // Something went wrong with our update. + console.log( error ); + } ); +}; + +// Event handler for handling mouse clicks that mark a task incomplete. +const markTaskIncomplete = () => { + apiFetch( { + path: '/wc-admin/options', + method: 'POST', + data: { woocommerce_admin_add_task_example_complete: false }, + } ) + .then( () => { + addTaskData.isComplete = false; + getHistory().push( getNewPath( {}, '/', {} ) ); + } ) + .catch( ( error ) => { + console.log( error ); + } ); +}; + +// Build the Task component. +const Task = () => { + return ( + + + Example task card content. +
+
+
+ { addTaskData.isComplete ? ( + + ) : ( + + ) } +
+
+
+ ); +}; + +// Use the 'woocommerce_admin_onboarding_task_list' filter to add a task. +addFilter( + 'woocommerce_admin_onboarding_task_list', + 'plugin-domain', + ( tasks ) => { + return [ + ...tasks, + { + key: 'example', + title: 'Example', + content: 'This is an example task.', + container: , + completed: addTaskData.isComplete, + visible: true, + additionalInfo: 'Additional info here', + time: '2 minutes', + isDismissable: true, + onDismiss: () => console.log( 'The task was dismissed' ), + }, + ]; + } +); +``` + +In the example above, the extension does a few different things. Let’s break it down: + +#### Handle imports + +First, import any functions, components, or other utilities from external dependencies. We’ve kept WooCommerce-related dependencies separate from others for the sake of keeping things tidy. In a real-world extension, you may be importing other local modules. In those cases, we recommend creating a visually separate section for those imports as well. + +```js +// External dependencies +import { addFilter } from '@wordpress/hooks'``; +import apiFetch from '@wordpress/api-fetch'``; +import { Card, CardBody } from '@wordpress/components'``; + +// WooCommerce dependencies +import { getHistory, getNewPath } from '@woocommerce/navigation'``; +``` + +The `addFilter` function allows us to hook in to JavaScript filters the same way that the traditional PHP call to `add_filter()` does. The `apiFetch` utility allows our extension to query the WordPress REST API without needing to deal with keys or authentication. Finally, the `Card` and `CardBody` are predefined React components that we’ll use as building blocks for our extension’s Task component. + +#### Create Event Handlers + +Next we define the logic for the functions that will handle events for our task. In the example above, we created two functions to handle mouse clicks that toggle the completion status of our task. + +```js +const markTaskComplete = () => { + apiFetch( { + path: '/wc-admin/options', + method: 'POST', + data: { woocommerce_admin_add_task_example_complete: true }, + } ) + .then( () => { + addTaskData.isComplete = true; + getHistory().push( getNewPath( {}, '/', {} ) ); + } ) + .catch( ( error ) => { + console.log( error ); + } ); +}; +``` + +In the example above, the event handler uses `apiFetch` to set the `woocommerce_admin_add_task_example_complete` option’s value to `true` and then updates the component’s state data and redirects the browser to the Admin root. In the case of an error, we’re simply logging it to the console, but you may want to implement your own solution here. + +The `markTaskIncomplete` function is more or less an inverse of `markTaskComplete` that toggles the task’s completion status in the opposite direction. + +#### Construct the component + +Next, we create a [functional component](https://reactjs.org/docs/components-and-props.html) that returns our task card. The intermixed JavaScript/HTML syntax we’re using here is called JSX. If you’re unfamiliar with it, you can [read more about it in the React docs](https://reactjs.org/docs/introducing-jsx.html). + +```js +const Task = () => { + return ( + + + Example task card content. +
+
+
+ { addTaskData.isComplete ? ( + + ) : ( + + ) } +
+
+
+ ); +}; +``` + +In the example above, we’re using the `Card` and `CardBody` components to construct our task’s component. The `div` inside the `CardBody` uses a [JavaScript expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Expressions) (`{}`) to embed a ternary operator that uses the component’s state to determine whether to display the task as complete or incomplete. + +#### Configure task and add it to the WooCommerce task list + +Finally, we’ll set some configuration values for our task and then use the `addFilter` function to append our task to the WooCommerce Admin Onboarding Task List. + +```js +addFilter( + 'woocommerce_admin_onboarding_task_list', + 'plugin-domain', + ( tasks ) => { + return [ + ...tasks, + { + key: 'example', + title: 'Example', + content: 'This is an example task.', + container: , + completed: addTaskData.isComplete, + visible: true, + additionalInfo: 'Additional info here', + time: '2 minutes', + isDismissable: true, + onDismiss: () => console.log( 'The task was dismissed' ), + }, + ]; + } +); +``` + +In the example above, we’re setting our task’s configuration as we pass it into the filter for simplicity, but in a real-world extension, you might encapsulate this somewhere else for better separation of concerns. Below is a list of properties that the task-list component supports for tasks. + +| Name | Type | Required | Description | +|----------------|------------|----------|-------------| +| key | String | Yes | Identifier | +| title | String | Yes | Task title | +| content | String | No | The content that will be visible in the Extensions setup list | +| container | Component | Yes | The task component that will be visible after selecting the item | +| completed | Boolean | Yes | Whether the task is completed or not | +| visible | Boolean | Yes | Whether the task is visible or not | +| additionalInfo | String | No | Additional information | +| time | String | Yes | Time it takes to finish up the task | +| isDismissable | Boolean | No | Whether the task is dismissable or not. If false the Dismiss button won’t be visible | +| onDismiss | Function | No | Callback method that it’s triggered on dismission | +| type | String | Yes | Type of task list item, setup items will be in the store setup and extension in the extensions setup | + + +--- + +## Using Store Management Links + +When a merchant completes all of the items on the onboarding task list, WooCommerce replaces it with a section containing a list of handy store management links. Discoverability can be a challenge for extensions, so this section is a great way to bring more attention to key features of your extension and help merchants navigate to them. + +The store management section has a relatively narrow purpose, so this section does not currently support external links. Instead, it is meant for navigating quickly within WooCommerce. + +Adding your own store management links is a simple process that involves: + +- Installing dependencies for icon support +- Enqueuing an admin script in your PHP +- Hooking in via a JavaScript filter to provide your link object + +### Installing the Icons package + +Store management links use the `@wordpress/icons` package. If your extension isn’t already using it, you’ll need to add it to your extension’s list of dependencies. + +`npm` `install` ` @wordpress``/icons ` `--save` + +### Enqueuing the JavaScript + +The logic that adds your custom link to the store management section will live in a JavaScript file. We’ll register and enqueue that file with WordPress in our PHP file: + +```js +function custom_store_management_link() { + wp_enqueue_script( + 'add-my-custom-link', + plugins_url( '/dist/add-my-custom-link.js', __FILE__ ), + array( 'wp-hooks' ), + 10 + ); +} +add_action( 'admin_enqueue_scripts', 'custom_store_management_link' ); + +``` + +The first argument of this call is a handle, the name by which WordPress will refer to the script we’re enqueuing. The second argument is the URL where the script is located. + +The third argument is an array of script dependencies. By supplying the `wp-hooks` handle in that array, we’re ensuring that our script will have access to the `addFilter` function we’ll be using to add our link to WooCommerce’s list. + +The fourth argument is a priority, which determines the order in which JavaScripts are loaded in WordPress. We’re setting a priority of 10 in our example. It’s important that your script runs before the store management section is rendered. With that in mind, make sure your priority value is lower than 15 to ensure your link is rendered properly. + +### Supply your link via JavaScript + +Finally, in the JavaScript file you enqueued above, hook in to the `woocommerce_admin_homescreen_quicklinks` filter and supply your task as a simple JavaScript object. + +```js +import { megaphone } from '@wordpress/icons'; +import { addFilter } from '@wordpress/hooks'; + +addFilter( + 'woocommerce_admin_homescreen_quicklinks', + 'my-extension', + ( quickLinks ) => { + return [ + ...quickLinks, + { + title: 'My link', + href: 'link/to/something', + icon: megaphone, + }, + ]; + } +); +``` + +--- + +## Using Admin Notes + +Admin Notes are meant for displaying insightful information about your WooCommerce store, extensions, activity, and achievements. They’re also useful for displaying information that can help with the day-to-day tasks of managing and optimizing a store. A good general rule is to use Admin Notes for information that is: + +1. Timely +2. Relevant +3. Useful + +With that in mind, you might consider using Admin Notes to celebrate a particular milestone that a merchant has passed, or to provide additional guidance about using a specific feature or flow. Conversely, you shouldn’t use Admin Notes to send repeated messages about the same topic or target all users with a note that is only relevant to a subset of merchants. It’s okay to use Admin Notes for specific promotions, but you shouldn’t abuse the system. Use your best judgement and remember the home screen is meant to highlight a store’s most important actionable tasks. + +Despite being a part of the new React-powered admin experience in WooCommerce, Admin Notes are available to developers via a standard PHP interface. + +The recommended approach for using Admin Notes is to encapsulate your note within its own class that uses the [NoteTraits](https://github.com/woocommerce/woocommerce-admin/blob/831c9ff13a862f22cf53d3ae676daeabbefe90ad/src/Notes/NoteTraits.php) trait included with WooCommerce Admin. Below is a simple example of what this might look like: + +```php +set_title( 'Getting Started' ); + + // Set our note's content. + $note->set_content( + sprintf( + 'Extension activated on %s.', $activated_time_formatted + ) + ); + + // In addition to content, notes also support structured content. + // You can use this property to re-localize notes on the fly, but + // that is just one use. You can store other data here too. This + // is backed by a longtext column in the database. + $note->set_content_data( (object) array( + 'getting_started' => true, + 'activated' => $activated_time, + 'activated_formatted' => $activated_time_formatted + ) ); + + // Set the type of the note. Note types are defined as enum-style + // constants in the Note class. Available note types are: + // error, warning, update, info, marketing. + $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + + // Set the type of layout the note uses. Supported layout types are: + // 'banner', 'plain', 'thumbnail' + $note->set_layout( 'plain' ); + + // Set the image for the note. This property renders as the src + // attribute for an img tag, so use a string here. + $note->set_image( '' ); + + + // Set the note name and source. You should store your extension's + // name (slug) in the source property of the note. You can use + // the name property of the note to support multiple sub-types of + // notes. This also gives you a handy way of namespacing your notes. + $note->set_source( 'inbox-note-example'); + $note->set_name( self::NOTE_NAME ); + + // Add action buttons to the note. A note can support 0, 1, or 2 actions. + // The first parameter is the action name, which can be used for event handling. + // The second parameter renders as the label for the button. + // The third parameter is an optional URL for actions that require navigation. + $note->add_action( + 'settings', 'Open Settings', '?page=wc-settings&tab=general' + ); + $note->add_action( + 'learn_more', 'Learn More', 'https://example.com' + ); + + return $note; + } +} + +function my_great_extension_activate() { + // This uses the functionality from the NoteTraits trait to conditionally add your note if it passes all of the appropriate checks. + ExampleNote::possibly_add_note(); +} +register_activation_hook( __FILE__, 'my_great_extension_activate' ); + +function my_great_extension_deactivate() { + // This uses the functionality from the NoteTraits trait to conditionally remove your note if it passes all of the appropriate checks. + ExampleNote::possibly_delete_note(); +} +register_deactivation_hook( __FILE__, 'my_great_extension_deactivate' ); +``` + + +### Breaking it down + +Let’s break down the example above to examine what each section does. + +#### Namespacing and feature availability checks + +First, we’re doing some basic namespacing and feature availability checks, along with a safeguard to make sure this file only executes within the WordPress application space. + +```php +namespace My\Wonderfully\Namespaced\Extension\Area; + +defined ( 'ABSPATH' ) || exit; + +if ( ! class_exists( 'Automattic\WooCommerce\Admin\Notes\Notes') || + ! class_exists( 'Automattic\WooCommerce\Admin\Notes\NoteTraits') ) { + return; +} + +if ( ! class_exists( 'WC_Data_Store' ) ) { + return; +} +``` + +#### Using Note and NoteTraits objects + +Next, we define a simple class that will serve as a note provider for our note. To create and manage note objects, we’ll import the `Note` and `NotesTraits` classes from WooCommerce Admin. + +```php +class ExampleNote { + + use Automatic\WooCommerce\Admin\Notes\Note; + use Automatic\WooCommerce\Admin\Notes\NoteTraits; + +} +``` + +#### Provide a unique note name + +Before proceeding, create a constant called `NOTE_NAME` and assign a unique note name to it. The `NoteTraits` class uses this constant for queries and note operations. + +`const NOTE_NAME = 'my-prefix-example-note';` + +#### Configure the note’s details + +Once you’ve set your note’s name, you can define and configure your note. The `NoteTraits` class will call `self::get_note()` when performing operations, so you should encapsulate your note’s instantiation and configuration in a static function called `get_note()` that returns a `Note` object. + +```php +public static function get_note() { + // We'll fill this in with logic that instantiates a Note object + // and sets its properties. +} +``` + +Inside our `get_note()` function, we’ll handle any logic for collecting data our Note may need to display. Our example note will include information about when the extension was activated, so this bit of code is just for demonstration. You might include other logic here depending on what data your note should contain. + +```php +$activated_time = current_time( 'timestamp', 0); +$activated_time_formatted = date( 'F jS', $activated_time ); + +``` + +Next, we’ll instantiate a new `Note` object. + +`$note = new Note();` + +Once we have an instance of the Note class, we can work with its API to set its properties, starting with its title. + +`$note->set_title( 'Getting Started' );` + +Then we’ll use some of the timestamp data we collected above to set the note’s content. + +```php +$note->set_content( + sprintf( + 'Extension activated on %s.', $activated_time_formatted + ) +); +``` + +In addition to regular content, notes also support structured content using the `content_data` property. You can use this property to re-localize notes on the fly, but that is just one use case. You can store other data here too. This is backed by a `longtext` column in the database. + +```php +$note->set_content_data( (object) array( + 'getting_started' => true, + 'activated' => $activated_time, + 'activated_formatted' => $activated_time_formatted +) ); +``` + +Next, we’ll set the note’s `type` property. Note types are defined as enum-style class constants in the `Note` class. Available note types are _error_, _warning_, _update_, _info_, and _marketing_. When selecting a note type, be aware that the _error_ and _update_ result in the note being shown as a Store Alert, not in the Inbox. It’s best to avoid using these types of notes unless you absolutely need to. + +`$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );` + +Admin Notes also support a few different layouts. You can specify `banner`, `plain`, or `thumbnail` as the layout. If you’re interested in seeing the different layouts in action, take a look at [this simple plugin](https://gist.github.com/octaedro/864315edaf9c6a2a6de71d297be1ed88) that you can install to experiment with them. + +We’ll choose `plain` as our layout, but it’s also the default, so we could leave this property alone and the effect would be the same. + +`$note->set_layout( 'plain' );` + +If you have an image that you want to add to your Admin Note, you can specify it using the `set_image` function. This property ultimately renders as the `src` attribute on an `img` tag, so use a string here. + +`$note->set_image( '' );` + +Next, we’ll set the values for our Admin Note’s `name` and `source` properties. As a best practice, you should store your extension’s name (i.e. its slug) in the `source` property of the note. You can use the `name` property to support multiple sub-types of notes. This gives you a handy way of namespacing your notes and managing them at both a high and low level. + +```php +$note->set_source( 'inbox-note-example'); +$note->set_name( self::NOTE_NAME ); +``` + +Admin Notes can support 0, 1, or 2 actions (buttons). You can use these actions to capture events that trigger asynchronous processes or help the merchant navigate to a particular view to complete a step, or even simply to provide an external link for further information. The `add_action()` function takes up to three arguments. The first is the action name, which can be used for event handling, the second renders as a label for the action’s button, and the third is an optional URL for actions that require navigation. + +```php +$note->add_action( + 'settings', 'Open Settings', '?page=wc-settings&tab=general' +); +$note->add_action( + 'learn_more', 'Learn More', 'https://example.com' +); +``` + +Finally, remember to have the `get_note()` function return the configured Note object. + +`return $note;` + +#### Adding and deleting notes + +To add and delete notes, you can use the helper functions that are part of the `NoteTraits` class: `possibly_add_note()` and its counterpart `possibly_delete_note()`. These functions will handle some of the repetitive logic related to note management and will also run checks to help you avoid creating duplicate notes. + +Our example extension ties these calls to activation and deactivation hooks for the sake of simplicity. While there are many events for which you may want to add Notes to a merchant’s inbox, deleting notes upon deactivation and uninstallation is an important part of managing your extension’s lifecycle. + +```php +function my_great_extension_activate() { + ExampleNote::possibly_add_note(); +} +register_activation_hook( __FILE__, 'my_great_extension_activate' ); + +function my_great_extension_deactivate() { + ExampleNote::possibly_delete_note(); +} +register_deactivation_hook( __FILE__, 'my_great_extension_deactivate' ); + +``` diff --git a/docs/extension-development/how-to-add-your-own-store-management-links.md b/docs/extension-development/how-to-add-your-own-store-management-links.md new file mode 100644 index 00000000000..84b35a04c98 --- /dev/null +++ b/docs/extension-development/how-to-add-your-own-store-management-links.md @@ -0,0 +1,73 @@ +# Adding store management links + +## Introduction + +In the new and improved WooCommerce home screen, there are two points of extensibility for plugin developers that have recently had some attention. The first is the setup task list, allowing you to remind the user of tasks they need to complete and keeping track of their progress for them. + +The second is the store management links section. Once the user has completed the setup tasks this will display for them. This section consolidates a list of handy navigation links that merchants can use to quickly find features in WooCommerce. + +Discoverability can be hard for users so this can be a great place to bring attention to the features of your plugin and allow users to easily find their way to the key functionality your plugin provides. + +Adding your own store management links is a simple process. + +## Add your own store management link + +Before we start, let's outline a couple of restrictions on this feature. + +Right now these links are designed to keep the user within WooCommerce, so it does not support external links. + +All the links you add will fall under a special category in the list called "Extensions". There is not currently any support for custom categories. + +With those things in mind, let's start. + +## Step 1 - Enqueue JavaScript + +Adding a store management link will all be done in JavaScript, so the first step is enqueuing your script that will add the store management link. The most important thing here is ensuring that your script runs before the store management link section is rendered. + +To ensure that your script runs before ours you'll need to enqueue it with a priority higher than 15. You'll also need to depend on `wp-hooks` to get access to `addFilter`. + +Example: + +```php +function enqueue_management_link_script() { + wp_enqueue_script( $script_name, $script_url, array( 'wp-hooks' ), 10 ); +} + +add_action( 'admin_enqueue_scripts', 'enqueue_management_link_script' ); +``` + +## Step 2 - Install @wordpress/icons + +To provide an icon of your choice for your store management link, you'll need to install `@wordpress/icons` in your JavaScript project: + +```sh +npm install @wordpress/icons --save +``` + +## Step 3 - Add your filter + +Your script will need to use `addFilter` to provide your custom link to the store management link section. And you'll need to import your icon of choice from `@wordpress/icons`. Here's an example: + +```js +import { megaphone } from "@wordpress/icons"; +import { addFilter } from "@wordpress/hooks"; + +addFilter( + "woocommerce_admin_homescreen_quicklinks", + "my-extension", + (quickLinks) => { + return [ + ...quickLinks, + { + title: "My link", + href: "link/to/something", + icon: megaphone, + }, + ]; + } +); +``` + +Here's a screen shot using our new custom store management link: + +![screen shot of custom store management link in wp-admin](https://i.imgur.com/yvXeSya.png) diff --git a/docs/extension-development/how-to-design-a-simple-extension.md b/docs/extension-development/how-to-design-a-simple-extension.md new file mode 100644 index 00000000000..f7fb19e5154 --- /dev/null +++ b/docs/extension-development/how-to-design-a-simple-extension.md @@ -0,0 +1,394 @@ +# How to design a simple extension + +## Introduction + +Building a WooCommerce extension that provides a first-class experience for merchants and shoppers requires a hybrid development approach combining PHP and modern JavaScript. The PHP handles the lifecycle and server-side operations of your extension, while the modern JavaScript lets you shape the appearance and behavior of its user interface. + +## The main plugin file + +Your extension’s main PHP file is a bootstrapping file. It contains important metadata about your extension that WordPress and WooCommerce use for a number of ecosystem integration processes, and it serves as the primary entry point for your extension’s functionality. While there is not a particular rule enforced around naming this file, using a hyphenated version of the plugin name is a common best practice. (i.e. my-extension.php) + +## Declaring extension metadata + +Your extension’s main plugin file should have a header comment that includes a number of important pieces of metadata about your extension. WordPress has a list of header requirements to which all plugins must adhere, but there are additional considerations for WooCommerce extensions: + +- The `Author` and `Developer` fields are required and should be set to + either your name or your company name. + +- The `Developer URI` field should be your official webpage URL. + +- The `Plugin URI` field should contain the URL of the extension’s product page in the WooCommerce Marketplace or the extension’s official landing page on your website. + +- For extensions listed in the WooCommerce Marketplace, to help facilitate the update process, add a `Woo` field and an appropriate value. WooCommerce Marketplace vendors can find this snippet by logging in to the Vendors Dashboard and navigating to `Extensions > All Extensions`. Then, select the product and click Edit product page. This snippet will be in the upper-right-hand corner of the screen. + +Below is an example of what the header content might look like for an extension listed in the WooCommerce Marketplace. + +```php +/** + * Plugin Name: My Great WooCommerce Extension + * Plugin URI: http://woocommerce.com/products/woocommerce-extension/ + * Description: Your extension's description text. + * Version: 1.0.0 + * Author: Your Name + * Author URI: http://yourdomain.com/ + * Developer: Your Name + * Developer URI: http://yourdomain.com/ + * Text Domain: my-extension + * Domain Path: /languages + * + * Woo: 12345:342928dfsfhsf8429842374wdf4234sfd + * + * License: GNU General Public License v3.0 + * License URI: http://www.gnu.org/licenses/gpl-3.0.html + */ +``` + +## Preventing data leaks + +As a best practice, your extension’s PHP files should contain a conditional statement at the top that checks for WordPress’ ABSPATH constant. If this constant is not defined, the script should exit. + +`defined( 'ABSPATH' ) || exit;` + +This check prevents your PHP files from being executed via direct browser access and instead only allows them to be executed from within the WordPress application environment. + +## Managing extension lifecycle + +Because your main PHP file is the primary point of coupling between your extension and WordPress, you should use it as a hub for managing your extension’s lifecycle. At a very basic level, this means handling: + +- Activation +- Execution +- Deactivation + +Starting with these three broad lifecycle areas, you can begin to break your extension’s functionality down further to help maintain a good separation of concerns. + +## Handling activation and deactivation + +A common pattern in WooCommerce extensions is to create dedicated functions in your main PHP file to serve as activation and deactivation hooks. You then register these hooks with WordPress using the applicable registration function. This tells WordPess to call the function when the plugin is activated or deactivated. Consider the following examples: + +```php +function my_extension_activate() { + // Your activation logic goes here. +} +register_activation_hook( __FILE__, 'my_extension_activate' ); +``` + +```php +function my_extension_deactivate() { + // Your deactivation logic goes here. +} +register_deactivation_hook( __FILE__, 'my_extension_deactivate' ); +``` + +## Maintaining a separation of concerns + +There are numerous ways to organize the code in your extension. You can find a good overview of best practices in the WordPress Plugin Developer Handbook. Regardless of the approach you use for organizing your code, the nature of WordPress’ shared application space makes it imperative that you build with an eye toward interoperability. There are a few common principles that will help you optimize your extension and ensure it is a good neighbor to others: + +- Use namespacing and prefixing to avoid conflicts with other extensions. +- Use classes to encapsulate your extension’s functionality. +- Check for existing declarations, assignments, and implementations. + +## The core extension class + +As mentioned above, encapsulating different parts of your extension’s functionality using classes is an important measure that not only helps with interoperability, but which also makes your code easier to maintain and debug. Your extension may have many different classes, each shouldering some piece of functionality. At a minimum, your extension should define a central class which can handle the setup, initialization and management of a single instance of itself. + +## Implementing a singleton pattern + +Unless you have a specific reason to create multiple instances of your main class when your extension runs, you should ensure that only one instance exists in the global scope at any time. A common way of doing this is to use a Singleton pattern. There are several ways to go about setting up a singleton in a PHP class. Below is a basic example of a singleton that also implements some of the best practices mentioned above about namespacing and pre-declaration checks: + +```php +if ( ! class_exists( 'My_Extension' ) ) : + /** + * My Extension core class + */ + class My_Extension { + /** + * The single instance of the class. + */ + protected static $_instance = null; + + /** + * Constructor. + */ + protected function __construct() { + // Instantiation logic will go here. + } + + /** + * Main Extension Instance. + * Ensures only one instance of the extension is loaded or can be loaded. + */ + public static function instance() { + if ( is_null( self::$_instance ) ) { + self::$_instance = new self(); + } + + return self::$_instance; + } + + /** + * Cloning is forbidden. + */ + public function __clone() { + // Override this PHP function to prevent unwanted copies of your instance. + // Implement your own error or use `wc_doing_it_wrong()` + } + + /** + * Unserializing instances of this class is forbidden. + */ + public function __wakeup() { + // Override this PHP function to prevent unwanted copies of your instance. + // Implement your own error or use `wc_doing_it_wrong()` + } + } +endif; +``` + +Notice that the example class above is designed to be instantiated by calling the static class method `instance()`, which will either return an existing instance of the class or create one and return it. In order to fully protect against unwanted instantiation, it’s also necessary to override the built-in magic methods `__clone()` and `__wakeup()`. You can implement your own error logging here or use something like `_doing_it_wrong()` which handles error logging for you. You can also use WooCommerce’s wrapper function `wc_doing_it_wrong()` here. Just be sure your code checks that the function exists first. + +## Constructor + +The example above includes an empty constructor for demonstration. In a real-world WooCommerce extension, however, this constructor should handle a few important tasks: + +- Check for an active installation of WooCommerce & other sibling dependencies. + +- Call a setup method that loads other files that your class depends on. +- Call an initialization method that gets your class and its dependencies ready to go. + +If we build upon our example above, it might look something like this: + +```php +protected function __construct() { + $this->includes(); + $this->init(); + // You might also include post-setup steps such as showing activation notices here. +} +``` + +## Loading dependencies + +The includes() function above is where you’ll load other class dependencies, typically via an include or require constructs. A common way of managing and loading external dependencies is to use Composer’s autoload feature, but you can also load specific files individually. You can read more about how to autoload external dependencies in the Composer documentation. A basic example of a setup method that uses both Composer and internal inclusion is below. + +```php +public function includes() { + $loader = include_once dirname( __FILE__ ) . '/' . 'vendor/autoload.php'; + + if ( ! $loader ) { + throw new Exception( 'vendor/autoload.php missing please run `composer install`' ); + } + + require_once dirname( __FILE__ ) . '/' . 'includes/my-extension-functions.php'; +} +``` + +## Initialization + +The `init()` function above is where you should handle any setup for the classes you loaded in the includes() method. This step is where you’ll often perform any initial registration with relevant actions or filters. It’s also where you can register and enqueue your extension’s JavaScripts and stylesheets. + +Here’s an example of what your initialization method might look like: + +```php +private function init() { + // Set up cache management. + new My_Extension_Cache(); + + // Initialize REST API. + new My_Extension_REST_API(); + + // Set up email management. + new My_Extension_Email_Manager(); + + // Register with some-action hook + add_action( 'some-action', 'my-extension-function' ); +} +``` + +There are many different ways that your core class’ initialization method might look, depending on the way that you choose to architect your extension. The important concept here is that this function serves as a central point for handling any initial registration and setup that your extension requires in order to respond to web requests going forward. + +## Delaying initialization + +The WordPress activation hook we set up above with register_activation_hook() may seem like a great place to instantiate our extension’s main class, and in some cases it will work. By virtue of being a plugin for a plugin, however, WooCommerce extensions typically require WooCommerce to be loaded in order to function properly, so it’s often best to delay instantiation and initialization until after WordPress has loaded other plugins. + +To do that, instead of hooking your instantiation to your extension’s activation hook, use the plugins_loaded action in WordPress to instantiate your extension’s core class and add its singleton to the $GLOBALS array. + +```php +function my_extension_initialize() { + // This is also a great place to check for the existence of the WooCommerce class + if ( ! class_exists( 'WooCommerce' ) ) { + // You can handle this situation in a variety of ways, + // but adding a WordPress admin notice is often a good tactic. + return; + } + + $GLOBALS['my_extension'] = My_Extension::instance(); +} +add_action( 'plugins_loaded', 'my_extension_initialize', 10 ); +``` + +In the example above, WordPress will wait until after all plugins have been loaded before trying to instantiate your core class. The third argument in add_action() represents the priority of the function, which ultimately determines the order of execution for functions that hook into the plugins_loaded action. Using a value of 10 here ensures that other WooCommerce-related functionality will run before our extension is instantiated. + +## Handling execution + +Once your extension is active and initialized, the possibilities are wide open. This is where the proverbial magic happens in an extension, and it’s largely up to you to define. While implementing specific functionality is outside the scope of this guide, there are some best practices to keep in mind as you think about how to build out your extension’s functionality. + +- Keep an event-driven mindset. Merchants and shoppers who use your extension will be interacting with WooCommerce using web requests, so it can be helpful to anchor your extension to some of the critical flows that users follow in WooCommerce. + +- Keep business logic and presentation logic separate. This could be as simple as maintaining separate classes for handling back-end processing and front-end rendering. + +- Where possible, break functionality into smaller parts and delegate responsibility to dedicated classes instead of building bloated classes and lengthy functions. + +You can find detailed documentation of classes and hooks in the WooCommerce Core Code Reference and additional documentation of the REST API endpoints in the WooCommerce REST API Documentation. + +## Handling deactivation + +The WordPress deactivation hook we set up earlier in our main PHP file with register_deactivation_hook() is a great place to aggregate functionality for any cleanup that you need to handle when a merchant deactivates your extension. In addition to any WordPress-related deactivation tasks your extension needs to do, you should also account for WooCommerce-related cleanup, including: + +- Removing Scheduled Actions +- Removing Notes in the Admin Inbox +- Removing Admin Tasks + +## Uninstallation + +While it’s certainly possible to completely reverse everything your extension has created when a merchant deactivates it, it’s not advisable nor practical in most cases. Instead, it’s best to reserve that behavior for uninstallation. + +For handling uninstallation, it’s best to follow the guidelines in the WordPress Plugin Handbook. + +## Putting it all together + +Below is an example of what a main plugin file might look like for a very simple extension: + +```php +/** + * Plugin Name: My Great WooCommerce Extension + * Plugin URI: http://woocommerce.com/products/woocommerce-extension/ + * Description: Your extension's description text. + * Version: 1.0.0 + * Author: Your Name + * Author URI: http://yourdomain.com/ + * Developer: Your Name + * Developer URI: http://yourdomain.com/ + * Text Domain: my-extension + * Domain Path: /languages + * + * Woo: 12345:342928dfsfhsf8429842374wdf4234sfd + * + * License: GNU General Public License v3.0 + * License URI: http://www.gnu.org/licenses/gpl-3.0.html + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Activation and deactivation hooks for WordPress + */ +function myPrefix_extension_activate() { + // Your activation logic goes here. +} +register_activation_hook( __FILE__, 'myPrefix_extension_activate' ); + +function myPrefix_extension_deactivate() { + // Your deactivation logic goes here. + + // Don't forget to: + // Remove Scheduled Actions + // Remove Notes in the Admin Inbox + // Remove Admin Tasks +} +register_deactivation_hook( __FILE__, 'myPrefix_extension_deactivate' ); + + +if ( ! class_exists( 'My_Extension' ) ) : + /** + * My Extension core class + */ + class My_Extension { + + /** + * The single instance of the class. + */ + protected static $_instance = null; + + /** + * Constructor. + */ + protected function __construct() { + $this->includes(); + $this->init(); + } + + /** + * Main Extension Instance. + */ + public static function instance() { + if ( is_null( self::$_instance ) ) { + self::$_instance = new self(); + } + return self::$_instance; + } + + /** + * Cloning is forbidden. + */ + public function __clone() { + // Override this PHP function to prevent unwanted copies of your instance. + // Implement your own error or use `wc_doing_it_wrong()` + } + + /** + * Unserializing instances of this class is forbidden. + */ + public function __wakeup() { + // Override this PHP function to prevent unwanted copies of your instance. + // Implement your own error or use `wc_doing_it_wrong()` + } + + /** + * Function for loading dependencies. + */ + private function includes() { + $loader = include_once dirname( __FILE__ ) . '/' . 'vendor/autoload.php'; + + if ( ! $loader ) { + throw new Exception( 'vendor/autoload.php missing please run `composer install`' ); + } + + require_once dirname( __FILE__ ) . '/' . 'includes/my-extension-functions.php'; + } + + /** + * Function for getting everything set up and ready to run. + */ + private function init() { + + // Examples include: + + // Set up cache management. + // new My_Extension_Cache(); + + // Initialize REST API. + // new My_Extension_REST_API(); + + // Set up email management. + // new My_Extension_Email_Manager(); + + // Register with some-action hook + // add_action('some-action', 'my-extension-function'); + } + } +endif; + +/** + * Function for delaying initialization of the extension until after WooComerce is loaded. + */ +function my_extension_initialize() { + + // This is also a great place to check for the existence of the WooCommerce class + if ( ! class_exists( 'WooCommerce' ) ) { + // You can handle this situation in a variety of ways, + // but adding a WordPress admin notice is often a good tactic. + return; + } + + $GLOBALS['my_extension'] = My_Extension::instance(); +} +``` diff --git a/docs/extension-development/readme.md b/docs/extension-development/readme.md new file mode 100644 index 00000000000..3032c6af86b --- /dev/null +++ b/docs/extension-development/readme.md @@ -0,0 +1,5 @@ +# 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! + +This section will provide comprehensive guidance on developing extensions for WooCommerce. You will be able to dive into methodologies, best practices, and discover tutorials to create robust, user-friendly extensions that enhance the capabilities of the WooCommerce platform. diff --git a/docs/extension-development/tools-for-low-code-development.md b/docs/extension-development/tools-for-low-code-development.md new file mode 100644 index 00000000000..61f45b8b9c0 --- /dev/null +++ b/docs/extension-development/tools-for-low-code-development.md @@ -0,0 +1,54 @@ +# Building blocks for low code builders + +## Introduction + +This guide provides an introduction to low-code solutions, such as Gutenberg and WooCommerce Store Editing, as well as other page builders and pre-built components for creating WooCommerce stores without custom coding. By understanding and leveraging these tools, low code builders can assemble stores with minimal coding and focus on the design and user experience aspects of their e-commerce websites. + +## Audience + +This guide is intended for low code builders or anyone with a basic understanding of WordPress and WooCommerce who wants to create online stores without the need for extensive custom coding. + +## Prerequisites + +To follow this guide, you should have: + +1. A basic understanding of WordPress and WooCommerce. +2. A WordPress website with WooCommerce installed and activated. + +## Step 1 — Using Gutenberg and WooCommerce Store Editing + +Gutenberg is the default block editor in WordPress that allows you to create and edit pages by adding and customizing blocks. WooCommerce has extended Gutenberg's functionality, enabling you to create and customize WooCommerce-specific elements, such as product pages and shop pages. + +To use Gutenberg and WooCommerce Store Editing: + +1. Ensure your WordPress installation is up-to-date to have the latest version of Gutenberg. +2. Install and activate a Block Theme to enable Store Editing features for WooCommerce. + +With Gutenberg and WooCommerce Store Editing, you can create and customize your store's pages using a wide variety of blocks, such as text, images, buttons, and WooCommerce-specific blocks like product grids and shopping carts. + +## Step 2 — Exploring alternative page builders + +While Gutenberg and WooCommerce Store Editing are powerful options for building low-code WooCommerce stores, you may also consider using other page builders for more advanced features or specific use cases. Some popular page builders compatible with WooCommerce include: + +1. Elementor +2. Beaver Builder +3. Divi Builder +4. WPBakery Page Builder + +Choose a page builder that fits your needs and budget, then install and activate it on your WordPress website. These page builders typically offer a library of pre-built components that you can use to create a fully functional WooCommerce store without writing custom code. + +## Step 3 — Utilizing pre-built components and templates + +Many page builders, including Gutenberg, offer pre-built components or blocks that can be easily added to your pages. These components can include design elements like buttons, forms, and image galleries, as well as WooCommerce-specific components like product grids and shopping carts. + +Additionally, some page builders and WooCommerce extensions offer pre-built store templates that you can import and customize to create a fully functional online store quickly. These templates can save you time and effort by providing a professionally designed starting point for your store. + +To use pre-built components and templates: + +1. Open your preferred page builder's editor (Gutenberg or another page builder). +2. Browse through the available components/blocks or templates and find the ones that suit your needs. +3. Add the components to your pages and customize them using the provided settings and options. + +## Conclusion + +By leveraging low-code solutions like Gutenberg, WooCommerce Store Editing, and other page builders, you can create a fully functional WooCommerce store without the need for custom coding. This guide has introduced you to the basics of using these tools, helping you understand the available options and assemble your store with minimal coding. With the right combination of tools and templates, you can create a professional, user-friendly e-commerce website that meets your business needs. diff --git a/docs/getting-started/developer-resources.md b/docs/getting-started/developer-resources.md new file mode 100644 index 00000000000..d88e8d0dded --- /dev/null +++ b/docs/getting-started/developer-resources.md @@ -0,0 +1,102 @@ +# 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. + +## Getting Started + +There are a few different ways you might want to get started utilizing WooCommerce. Choose a path below to start developing based on your code comfort level! + +### [Installing and setting up WooCommerce](https://woocommerce.com/document/build-online-store/) + +If you’re brand new to Woo, this guide will show you How to build an online store on WooCommerce. This is where you can learn the ins and outs of how WooCommerce works before you start developing. + +### [Extension Development Quick Start](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/create-woo-extension) + +This no-configuration quick-start package will scaffold a local copy of an extension template for you. Just open up your terminal and follow the steps in GitHub. + +### [Building your first extension](/extension-development/building-your-first-extension.md) + +This guide will have you building your first extension with best practices and helpful tips. + +### [Marketplace Contribution Guidelines](https://woocommerce.com/document/marketplace-overview/) + +Are you hoping to sell your extension in the [Woo Marketplace](https://woocommerce.com/marketplace/)? Read our guidelines to make sure your extension is marketplace-ready. + +### [Contributor Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) + +If you've ever wanted to contribute to the WooCommerce platform as a developer please read our guidelines for contribution first. + +### [Contribution Environment Set-Up](https://github.com/woocommerce/woocommerce/tree/trunk) + +Visit the WooCommerce home repository on GitHub to learn the first steps to environment set up and platform contribution expectations. + +### [Developer tools](/getting-started/developer-tools.md) + +Check out our guide to learn more about developer tools, libraries, and utilities. + +--- + + +## API & Reference Docs + +The resources below contain low-level documentation about features, libraries, extensions, and other pieces of WooCommerce architecture. Use them as a reference when building extensions or integrating with WooCommerce. + +## [REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/) + +The WooCommerce REST API lets you create, read, update, and delete WooCommerce data using HTTP requests, so you can integrate external applications with WooCommerce and build extensions that make use of asynchronous UI frameworks such as React. + +### [Core API](https://docs.woocommerce.com/wc-apidocs/index.html) + +The WooCommerce Core API code reference contains information about packages and classes that make up WooCommerce's core functionality. + +### [Store API](https://github.com/woocommerce/woocommerce-blocks/tree/trunk/src/StoreApi) + +The Store API provides public Rest API endpoints for the development of customer-facing cart, checkout, and product functionality. It follows many of the patterns used in the [WordPress REST API](https://developer.wordpress.org/rest-api/key-concepts/). + +### [WooCommerce Blocks](https://github.com/woocommerce/woocommerce-gutenberg-products-block/#documentation) + +WooCommerce Blocks give you the ability to integrate WooCommerce with Gutenberg. Use the documentation and resources here as a starting point for developing new block types for WooCommerce. + +### [Core Action and Filter Hooks](https://docs.woocommerce.com/wc-apidocs/hooks/hooks.html) + +This contains an index of hooks found across all template files, functions, shortcodes, widgets, data stores, and core classes. You can use these hooks to extend the core WooCommerce platform by introducing custom behavior or modifying data that WooCommerce passes around. + +### [Shortcodes Included with WooCommerce](https://docs.woocommerce.com/document/woocommerce-shortcodes/) + +While WooCommerce Blocks are now the easiest and most flexible way to display your products on posts and pages, WooCommerce still comes with several shortcodes to insert content. + +--- + +## GitHub Repositories + +### [WooCommerce on GitHub](https://github.com/woocommerce) + +This is the official WooCommerce organization on GitHub. Here you’ll find the majority of development work that happens on open source projects that the WooCommerce team maintains. + +### [Automattic on GitHub](https://github.com/automattic) + +This is the official Automattic organization on GitHub. It is where you'll find the majority of development work that happens on open source projects that the Automattic team maintains. + +### [WordPress on GitHub](https://github.com/wordpress) + +This is the official WordPress organization on GitHub –– a go-to source for the development work that happens on open source projects that the WordPress community maintains. + +--- + +## Ecosystem Resources + +### [WordPress Developer Resources](https://developer.wordpress.org/) + +All the resources you need for developing with WordPress. If you’re not familiar with the WordPress development ecosystem, this is a great place to start. + +### [WooCommerce Community Slack](https://woocommerce.com/community-slack) + +Join our community on Slack. We hold regular sessions where we share information and field questions, but you can also connect with other developers to share challenges and ask questions. + +### [WooCommerce Community Forum](https://wordpress.org/support/plugin/woocommerce/) + +Use this forum to ask questions about WooCommerce. Our WooCommerce Happiness Engineers frequent this forum to answer questions, but there is also a wealth of knowledge that has been captured in these threads over the years. + +### [WooCommerce on Reddit](https://www.reddit.com/r/woocommerce/) + +Visit the WooCommerce subreddit to ask questions and share tips with other developers. diff --git a/docs/getting-started/developer-tools.md b/docs/getting-started/developer-tools.md new file mode 100644 index 00000000000..4fcb406f13d --- /dev/null +++ b/docs/getting-started/developer-tools.md @@ -0,0 +1,89 @@ +# 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. + +## Table of Contents + +- [Productivity Tools](#productivity-tools) +- [Libraries](#libraries) +- [Utilities](#utilities) + +### Productivity Tools + +Use these resources to get a WooCommerce development environment up and running. + +#### [wp-cli](https://wp-cli.org/) + +This is the command-line interface for [WordPress](https://wordpress.org/). You can update plugins, configure multisite installations and much more, without using a web browser. + +#### [wp-env](https://www.npmjs.com/package/@wordpress/env) + +This command-line tool lets you easily set up a local WordPress environment for building and testing plugins and themes. It’s simple to install and requires no configuration. + +#### [eslint-plugin](https://www.npmjs.com/package/@woocommerce/eslint-plugin) + +This is an [ESLint](https://eslint.org/) plugin including configurations and custom rules for WooCommerce development. + +#### [e2e-environment](https://www.npmjs.com/package/@woocommerce/e2e-environment) + +This is a reusable and extensible end-to-end testing environment for WooCommerce extensions. Additionally, it contains several files to serve as the base for a Docker container and Travis CI setup. + +#### [WordPress Scripts](https://www.npmjs.com/package/@wordpress/scripts) + +This is a collection of reusable scripts tailored for WordPress development. + +--- + +### Libraries + +Use these resources to help take some of the heavy lifting off of fetching and transforming data –– as well as creating UI elements. + +#### API Clients + +#### [WooCommerce REST API — JavaScript](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) + +The official JavaScript library for working with the WooCommerce REST API. + +#### [api-fetch](https://www.npmjs.com/package/@wordpress/api-fetch) + +This is a utility to make WordPress REST API requests. It's a wrapper around `window.fetch` that includes support for nonces, middleware, and custom fetch handlers. + +#### Components + +#### [WooCommerce Components](https://www.npmjs.com/package/@woocommerce/components) + +This package includes a library of React components that can be used to create pages in the WooCommerce admin area. + +#### [WordPress Components](https://www.npmjs.com/package/@wordpress/components) + +This packages includes a library of generic WordPress components that can be used for creating common UI elements shared between screens and features of the WordPress dashboard. + +--- + +### Utilities + +#### [CSV Export](https://www.npmjs.com/package/@woocommerce/csv-export) + +A set of functions to convert data into CSV values, and enable a browser download of the CSV data. + +#### [Currency](https://www.npmjs.com/package/@woocommerce/currency) + +A collection of utilities to display and work with currency values. + +#### [Data](https://www.npmjs.com/package/@woocommerce/data) + +Utilities for managing the WooCommerce Admin data store. + +#### [Date](https://www.npmjs.com/package/@woocommerce/date) + +A collection of utilities to display and work with date values. + +#### [Navigation](https://www.npmjs.com/package/@woocommerce/navigation) + +A collection of navigation-related functions for handling query parameter objects, serializing query parameters, updating query parameters, and triggering path changes. + +#### [Number](https://www.npmjs.com/package/@woocommerce/number) + +A collection of utilities to properly localize numerical values in WooCommerce. + + diff --git a/docs/getting-started/readme.md b/docs/getting-started/readme.md new file mode 100644 index 00000000000..e1d1d35652f --- /dev/null +++ b/docs/getting-started/readme.md @@ -0,0 +1,5 @@ +# 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! + +Jumpstart your journey with WooCommerce. This category will cover the basics and essentials, from installation to initial setup, ensuring you have a solid foundation to build upon. diff --git a/docs/HPOS.md b/docs/high-performance-order-storage-(HPOS)/HPOS.md similarity index 99% rename from docs/HPOS.md rename to docs/high-performance-order-storage-(HPOS)/HPOS.md index 1fa1573b325..9fbb4c62247 100644 --- a/docs/HPOS.md +++ b/docs/high-performance-order-storage-(HPOS)/HPOS.md @@ -1,4 +1,4 @@ -# HPOS +# High Performance Order Storage (HPOS) WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues, and that's why HPOS (High-Performance Order Storage) was developed. HPOS is the WooCommerce engine that stores orders in dedicated tables. diff --git a/docs/high-performance-order-storage-(HPOS)/readme.md b/docs/high-performance-order-storage-(HPOS)/readme.md new file mode 100644 index 00000000000..0058c2cf036 --- /dev/null +++ b/docs/high-performance-order-storage-(HPOS)/readme.md @@ -0,0 +1,5 @@ +# High Performance Order Storage + +> ⚠️ **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! + +This section is where you can learn about High-Performance Order Storage (HPOS): a new database storage for orders to allow effortless scaling for large and high growth stores. \ No newline at end of file diff --git a/docs/quality-and-best-practices/coding-standards.md b/docs/quality-and-best-practices/coding-standards.md new file mode 100644 index 00000000000..83948d60db7 --- /dev/null +++ b/docs/quality-and-best-practices/coding-standards.md @@ -0,0 +1,67 @@ +# Coding standards for the code snippets within the WooCommerce documentation + +## Position of hooks + +Position hooks below the function call, as this follows the common pattern in the WordPress and WooCommerce ecosystem. + +### Example + +```php +/** + * Add custom message. + */ +function YOUR_PREFIX_custom_message() { + echo 'This is a custom message'; +} +add_action( 'wp_footer', 'YOUR_PREFIX_custom_message' ); +``` + +## Prefixing function calls + +Use a consistent prefix for all function calls. For the code snippets in this repo, use the prefix `YOUR_PREFIX`. + +### Example + +```php +/** + * Add custom discount. + */ +function YOUR_PREFIX_custom_discount( $price, $product ) { + return $price * 0.9; // 10% discount +} +add_filter( 'woocommerce_product_get_price', 'YOUR_PREFIX_custom_discount', 10, 2 ); +``` + +## Translatable texts and text domains + +Make all plain texts translatable, and use a consistent text domain. This aligns with the best practices for internationalisation. For the code snippets in this repo, use the textdomain `YOUR-TEXTDOMAIN`. + +### Example + +```php +/** + * Add custom message. + */ +function YOUR_PREFIX_welcome_message() { + echo __( 'Welcome to our website', 'YOUR-TEXTDOMAIN' ); +} +add_action( 'wp_footer', 'YOUR_PREFIX_welcome_message' ); +``` + +## Use of function_exists() + +Wrap all function calls in a `function_exists()` call to prevent errors due to potential function redeclaration. + +### Example + +```php +/** + * Add thumbnail support. + */ +if ( ! function_exists( 'YOUR_PREFIX_theme_setup' ) ) { + function YOUR_PREFIX_theme_setup() { + add_theme_support( 'post-thumbnails' ); + } +} +add_action( 'after_setup_theme', 'YOUR_PREFIX_theme_setup' ); +``` diff --git a/docs/quality-and-best-practices/grammar-punctuation-capitalization.md b/docs/quality-and-best-practices/grammar-punctuation-capitalization.md new file mode 100644 index 00000000000..2489cf26396 --- /dev/null +++ b/docs/quality-and-best-practices/grammar-punctuation-capitalization.md @@ -0,0 +1,321 @@ +# WooCommerce grammar, punctuation and capitalization guide + +Following grammar, punctuation and style guidelines helps keep our presentation consistent. Users have a better experience if they know what to expect and where to find the information they need. + +## Basics + +**Be democratic**. Some people read every word. Some scan and search or prefer video. Help everyone. + +**Be focused**. Lead with the most important information in sentences, paragraphs, and sections. + +**Be concise**. Use plain language and brief sentences. + +**Be consistent**. Follow our guidelines and style tips. + +**Be specific**. Communicate crystal clear. Trim the fat. + +## Guidelines + +### Abbreviations and acronyms + +Spell out the full version on first mention with abbreviation or acronym in parentheses. Use the short version on second and consecutive mentions. + +- First use: Payment Card Industry Data Security Standard (PCI-DSS) +- Second use: PCI-DSS + +If the abbreviation or acronym is widely known, use it as is. For example: API, FAQ, HTML, PHP, SQL, SSL. + +### Active voice + +With active voice, the subject in the sentence performs the action. With passive voice, the subject in the sentence has the action done unto it. + +- Active: Jon downloaded his extension files. +- Passive: The extension files were downloaded by Jon. + +### Capitalization + +Cases when we capitalize: + +- Blog post and documentation article titles: First word. +- Documentation headings (h2): Use sentence case (not title case) for docs titles and subheadings. +- Product names: Every word except prepositions and conjunctions. +- Sentences: First word. +- Unordered/Bulleted lists – First word of each entry. + +Cases when we use lower case: + +- “ecommerce” (not “eCommerce”) +- email address — info@woocommerce.com +- website URL — developer.woocommerce.com + +### Contractions + +Use with discretion. Contractions, such as I’m and there’s, give writing an informal and conversational feel, but may be inappropriate if content is being translated. For example, sometimes the not in don’t is ignored by online translators. + +### Emoji + +Emoji can add subtle emotion and humor or bring visual attention to your content. Use rarely and intentionally. + +### Numbers + +Spell out a number at the start of a sentence, and spell out numbers one through nine in all cases. Use numerals in all other cases. + +- Ten products will launch in June. Not: 10 products will launch in June. +- Lance ran a marathon and won third place in his age group. +- I bought five hammers and 21 types of nails for the building project. +- There were 18 kinds of beer on tap at the pub. + +Use a comma for numbers with more than three digits: 41,500, 170,000, 1,000,000 or 1 million. + +#### Currency + +Use currency codes and not only the symbol/sign when specifying dollars. Whole amounts need not have a decimal and two places. + +- USD $20 +- CAD $19.99 +- AUD $39.50 + +When writing about other currencies, use the symbol/sign. + +- €995 +- ¥5,000 +- £18.99 + +#### Dates + +Spell out the day of the week and month, using the format: + +- Monday, December 12, 2016 + +#### Decimals + +Use decimal points when a number is difficult to convert to a fraction, such as 3.141 or 98.5 or 0.29. + +#### Fractions + +Spell out fractions: one-fourth + +#### Percent + +Spell out the word ‘percent.’ Don’t use % symbol unless space is limited, e.g., for use on social media. + +#### Phone numbers + +Use hyphens without spaces between numbers, not parentheses or periods. Use a [country code](https://countrycode.org/) for all countries. + +- +1-555-867-5309 +- +34-902-1899-00 + +#### Range and span + +Use a hyphen to indicate a range or span of numbers: 20-30 days. + +#### Temperature + +Use the degree symbol and the capital C abbreviation for Celsius and capital F abbreviation for Fahrenheit. + +- 27°C +- 98°F + +#### Times + +Use numbers and am or pm with a space and without periods. + +- 7:00 am +- 7:30 pm + +Use a hyphen between times to indicate a time period in am or pm. Use ‘to’ if the time period spans am and pm. + +- 7:00-9:00 am and 7:00 am to 10:30 pm + +Specify a time zone when writing about an event with potential attendees worldwide. Automattic uses Coordinated Universal Time (UTC). + +Abbreviate U.S. time zones: + +- Eastern time: EDT or EST +- Central time: CDT or CST +- Mountain time: MDT or MST +- Pacific time: PDT or PST + +#### Years + +Abbreviate decades + +- 80s and 90s +- 1900s and 1890s + +### Punctuation + +#### Ampersands + +Ampersands need only be used when part of an official company/brand name. Should not be substituted for ‘and.’ + +- Ben & Jerry’s +- Andre, Timo, and Donny went to a football game at Camp Nou. + +#### Apostrophes + +An apostrophe makes a word possessive. If a word already ends in s and is singular, add an ‘s. If a word ends in s and is plural, add an apostrophe. + +- A teammate borrowed Sam’s bike. +- A teammate borrowed Chris’s bike. +- Employees hid the office managers’ pens. + +These are possessives: FAQ’s questions, HE’s weekly rotation. These are plural: FAQs and HEs. + +#### Colons + +Use a colon to create a list. + +- Aaron ordered three kinds of donuts: glazed, chocolate, and pumpkin. + +#### Commas + +Use a serial comma, also known as an Oxford comma, when compiling a list. + +- Jinny likes sunflowers, daisies, and peonies. + +Use common sense for other cases. Read the sentence out loud, and use a comma where clarity or pause may be needed. + +#### Dashes and hyphens + +Use a hyphen – without spaces on either side to link words, or indicate a span or range. + +- first-time user +- Monday-Friday + +Use an em dash — without spaces on either side to indicate an aside. + +Use a true em dash, not hyphens – or –. + +- Multivariate testing—just one of our new Pro features—can help you grow your business. +- Austin thought Brad was the donut thief, but he was wrong—it was Lain. + +#### Ellipses + +Ellipses … can be used to indicate an indefinite ending to a sentence or to show words are omitted when used in brackets […] Use rarely. + +#### Exclamation points + +Use an exclamation point rarely and use only one. + +Exclamation points follow the same placement convention explained in Periods. + +#### Periods + +Periods should be: + +- Inside quotation marks +- Outside parentheses when the portion in parentheses is part of a larger sentence +- Inside parentheses when the part in parentheses can stand on its own + +Examples + +- Jake said, “I had the best day ever.” +- She went to the supermarket (and to the nail salon). +- My mom loves pizza and beer. (Beer needs to be cold and dark.) + +#### Question marks + +Question marks follow the same placement convention explained in Periods. + +#### Quotation marks + +Periods and commas go within quotation marks. Question marks within quotes follow logic—if the question mark is part of the quotation, it goes within. If you’re asking a question that ends with a quote, it goes outside the quote. + +Use single quotation marks for quotes within quotes. + +- Who sings, “All These Things That I’ve Done”? +- Brandon Flowers of The Killers said, “I was inspired and on a roll when I wrote, ‘I got soul, but I’m not a soldier.’” + +#### Semicolons + +Semicolons can be used to join two related phrases. + +- Their debut solo album hit the Top 10 in 20 countries; it was #1 in the UK. + +### People, places, and things + +#### Company names and products + +Use brand identity names and products as written on official websites. + +- Nestlé +- Pull&Bear +- UE Boom + +Refer to a company or product as ‘it (not ‘they’). + +- WooCommerce is, and not WooCommerce are. + +#### File extensions + +A file extension type should be all uppercase without periods. Add a lowercase s to make plural. + +- HTML +- JPEG +- PDF + +A specific file should have a lowercase extension type: + +- dancingcat.gif +- SalesReport2016.pdf +- firethatcannon.mp3 + +#### Names and titles + +First mention of a person should include their first and last name. Second and consecutive mentions can use first name only. + +Capitalize job titles, the names of teams, and departments. + +- Happiness Engineers or HEs +- Team Apollo +- Legal + +#### Pronouns + +Use he/him/his and she/her/her as appropriate. Don’t use “one” as a pronoun. Use they/them/their if gender is unknown or when referring to a group. + +#### Quotations + +Use present tense when quoting someone. + +- “I love that WooCommerce is free and flexible,” says Brent Jamison. + +#### Schools + +The first time you mention a school, college, or university in a piece of writing, refer to it by its full official name. On all other mentions, use its more common abbreviation. + +- Georgia Institute of Technology, Georgia Tech +- Georgia State University, GSU + +#### States, cities, and countries + +Spell out all city and state names. Don’t abbreviate city names. + +On first mention, write out United States. For further mentions, use U.S. The same applies to other countries or federations with a common abbreviation, such as European Union (EU) and United Kingdom (UK). + +#### URLs and websites + +Capitalize the names of websites and web publications. Don’t italicize. + +Avoid writing out URLs; omit `http://www` when it’s necessary. + +### Slang and jargon + +Write in plain English. Text should be universally understood, with potential for translation. Briefly define technical terms when needed. + +### Text formatting + +Use italics to indicate the title of a book, movie, or album. + +- The Oren Klaff book Pitch Anything is on sale for USD $5.99. + +Avoid: + +- Underline formatting +- A mix of italic, bold, caps, and underline + +Left-align text, never center or right-aligned. + +Leave one space between sentences, never two. diff --git a/docs/quality-and-best-practices/performance-optimization.md b/docs/quality-and-best-practices/performance-optimization.md new file mode 100644 index 00000000000..dfdf63a9af5 --- /dev/null +++ b/docs/quality-and-best-practices/performance-optimization.md @@ -0,0 +1,89 @@ +# Performance optimization for WooCommerce stores + +## Introduction + +This guide covers best practices and techniques for optimizing the performance of WooCommerce stores, including caching, image optimization, database maintenance, code minification, and the use of Content Delivery Networks (CDNs). By following these recommendations, developers can build high-performing WooCommerce stores that provide a better user experience and contribute to higher conversion rates. + +## Audience + +This guide is intended for developers who are familiar with WordPress and WooCommerce and want to improve the performance of their online stores. + +## Prerequisites + +To follow this guide, you should have: + +1. A basic understanding of WordPress and WooCommerce. +2. Access to a WordPress website with WooCommerce installed and activated. + +## Step 1 — Implement caching + +Caching plays a crucial role in speeding up your WooCommerce store by serving static versions of your pages to visitors, reducing the load on your server. There are several ways to implement caching for your WooCommerce store: + +### Server-Side caching + +Enable server-side caching through your hosting provider or by using server-level caching solutions like Varnish, NGINX FastCGI Cache, or Redis. + +### WordPress caching plugins + +Install and configure a WordPress caching plugin, such as WP Rocket, W3 Total Cache, or WP Super Cache. These plugins can help you set up page caching, browser caching, and object caching for your WooCommerce store. + +### WooCommerce-Specific caching + +Ensure that your caching solution is configured correctly for WooCommerce, allowing dynamic content such as cart and checkout pages to remain uncached. Some caching plugins, like WP Rocket, include built-in support for WooCommerce caching. + +## Step 2 — Optimize images + +Optimizing images can significantly improve your store's performance by reducing the size of image files without compromising quality. To optimize images for your WooCommerce store: + +1. Use the right image format: Choose an appropriate format for your images, such as JPEG for photographs and PNG for graphics with transparency. +2. Compress images: Use an image compression tool like TinyPNG or ShortPixel to reduce file sizes before uploading them to your store. +3. Enable lazy loading: Lazy loading delays the loading of images until they're needed, improving initial page load times. Many caching plugins and performance optimization plugins offer built-in lazy loading options. +4. Use responsive images: Ensure that your theme and plugins serve appropriately sized images for different devices and screen resolutions. + +## Step 3 — Minify and optimize code + +Minifying and optimizing your store's HTML, CSS, and JavaScript files can help reduce file sizes and improve page load times. To minify and optimize code for your WooCommerce store: + +1. Use a plugin: Install a performance optimization plugin like Autoptimize, WP Rocket, or W3 Total Cache to minify and optimize your store's HTML, CSS, and JavaScript files. +2. Combine and inline critical CSS: Where possible, combine and inline critical CSS to reduce the number of requests and improve page load times. +3. Defer non-critical JavaScript: Defer loading of non-critical JavaScript files to improve perceived page load times. + +## Step 4 — Use a content delivery network (CDN) + +A Content Delivery Network (CDN) can help speed up your WooCommerce store by serving static assets like images, CSS, and JavaScript files from a network of servers distributed across the globe. To use a CDN for your WooCommerce store: + +1. Choose a CDN provider: Select a CDN provider like Cloudflare, Fastly, or Amazon CloudFront that fits your needs and budget. +2. Set up your CDN: Follow your chosen CDN provider's instructions to set up and configure the CDN for your WooCommerce store. + +## Step 5 — Optimize database + +Regularly optimizing your WordPress database can help improve your WooCommerce store's performance by removing unnecessary data and optimizing database tables. To optimize your database: + +1. Use a plugin: Install a database optimization plugin like WP-Optimize, WP-Sweep, or Advanced Database Cleaner to clean up and optimize your WordPress database. +2. Remove unnecessary data: Regularly delete spam comments, post revisions, and expired transients to reduce database clutter. +3. Optimize database tables: Use the database optimization plugin to optimize your database tables, improving their efficiency and reducing query times. + +## Step 6 — Choose a high-performance theme and plugins + +The theme and plugins you choose for your WooCommerce store can have a significant impact on performance. To ensure your store runs efficiently: + +1. Select a lightweight, performance-optimized theme: Choose a theme specifically designed for WooCommerce that prioritizes performance and follows best coding practices. +2. Evaluate plugin performance: Use tools like Query Monitor or WP Hive to analyze the performance impact of the plugins you install, and remove or replace those that negatively affect your store's performance. + +## Step 7 — Enable GZIP compression + +GZIP compression can help reduce the size of your store's HTML, CSS, and JavaScript files, leading to faster page load times. To enable GZIP compression: + +1. Use a plugin: Install a performance optimization plugin like WP Rocket, W3 Total Cache, or WP Super Cache that includes GZIP compression options. +2. Configure your server: Alternatively, enable GZIP compression directly on your server by modifying your .htaccess file (for Apache servers) or nginx.conf file (for NGINX servers). + +## Step 8 — Monitor and analyze performance + +Continuously monitor and analyze your WooCommerce store's performance to identify potential bottlenecks and areas for improvement. To monitor and analyze performance: + +1. Use performance testing tools: Regularly test your store's performance using tools like Google PageSpeed Insights, GTmetrix, or WebPageTest. +2. Implement performance monitoring: Install a performance monitoring plugin like New Relic or use a monitoring service like Uptime Robot to keep track of your store's performance over time. + +## Conclusion + +By following these best practices and techniques for performance optimization, you can build a high-performing WooCommerce store that offers a better user experience and contributes to higher conversion rates. Continuously monitor and analyze your store's performance to ensure it remains optimized as your store grows and evolves. diff --git a/docs/quality-and-best-practices/readme.md b/docs/quality-and-best-practices/readme.md new file mode 100644 index 00000000000..1fc1dc28742 --- /dev/null +++ b/docs/quality-and-best-practices/readme.md @@ -0,0 +1,5 @@ +# 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! + +Ensuring the quality of your WooCommerce projects is essential. This section will delve into quality exoectations, best practices, coding standards, and other methodologies to ensure your projects stand out in terms of reliability, efficiency, user experience, and more. diff --git a/docs/reference-code/readme.md b/docs/reference-code/readme.md new file mode 100644 index 00000000000..936a54845ed --- /dev/null +++ b/docs/reference-code/readme.md @@ -0,0 +1,5 @@ +# 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. diff --git a/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md b/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md new file mode 100644 index 00000000000..5e0727131ea --- /dev/null +++ b/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md @@ -0,0 +1,86 @@ +# Adding columns to analytics reports and CSV downloads + +Adding columns to analytics reports are a really interesting way to add functionality to WooCommerce. New data can be consumed in the table view of the user interface and in your user's favourite spreadsheet or third party application by generating a CSV. + +These instructions assume that you have a test plugin for WooCommerce installed and activated. You can follow the ["Getting started" instructions](extending-woocommerce-admin-reports.md) to get a test plugin set up. That post also includes instructions to further modify the query that is executed to get the data in an advanced fashion - it isn't required to just add a simple column. + +In WooCommerce, analytics CSVs are generated in two different ways: in the web browser using data already downloaded, or on the server using a new query. It uses the size of the data set to determine the method - if there is more than one page worth of results it generates the data on the server and emails a link to the user, but if the results fit on one page the data is generated and downloaded straight away in the browser. + +We'll look at the on-server method for adding a column first, because this is also where the data sent to the browser is generated. + +This example extends the Downloads analytics report. To get some data in your system for this report, create a downloadable product with a download expiry value, create an order purchasing the product, then download the product several times. In testing I created 26 downloads, which is enough that the report is spread over two pages when showing 25 items per page, and on a single page when showing 50 items per page. This let me test CSVs generated both on the server and in browser. + +In the PHP for your plugin, add three filter handlers: + +```php +// This adds the SELECT fragment to the report SQL +function add_access_expires_select( $report_columns, $context, $table_name ) { + if ( $context !== 'downloads' ) { + return $report_columns; + } + $report_columns['access_expires'] = + 'product_permissions.access_expires AS access_expires'; + return $report_columns; +} +add_filter( 'woocommerce_admin_report_columns', 'add_access_expires_select', 10, 3 ); + +// This adds the column header to the CSV +function add_column_header( $export_columns ) { + $export_columns['access_expires'] = 'Access expires'; + return $export_columns; +} +add_filter( 'woocommerce_filter_downloads_export_columns', 'add_column_header' ); + +// This maps the queried item to the export item +function map_access_expires( $export_item, $item ) { + $export_item['access_expires'] = $item['access_expires']; + return $export_item; +} +add_filter( 'woocommerce_report_downloads_prepare_export_item', 'map_access_expires', 10, 2 ); +``` + +This adds the access expiry timestamp to the Downloads table/CSV (when the CSV is generated on the server). + +These three filters together add the new column to the database query, adds the new header to the CSV, and maps the data returned from the database to the CSV. The first filter `woocommerce_admin_report_columns` adds a SQL fragment to the `SELECT` statement generated for the data query. The second filter `woocommerce_filter_downloads_export_columns` adds the column header to the CSV generated on the server. The third filter `woocommerce_report_downloads_prepare_export_item` maps the value in the data returned from the database query `$item` to the export item for the CSV. + +To finish this off by adding support for columns generated in browser, another filter needs to be added to your plugin's JavaScript: + +```js +import { addFilter } from "@wordpress/hooks"; +function addAccessExpiresToDownloadsReport(reportTableData) { + const { endpoint, items } = reportTableData; + if ("downloads" !== endpoint) { + return reportTableData; + } + + reportTableData.headers = [ + ...reportTableData.headers, + { + label: "Access expires", + key: "access_expires", + }, + ]; + reportTableData.rows = reportTableData.rows.map((row, index) => { + const item = items.data[index]; + const newRow = [ + ...row, + { + display: item.access_expires, + value: item.access_expires, + }, + ]; + return newRow; + }); + + return reportTableData; +} +addFilter( + "woocommerce_admin_report_table", + "dev-blog-example", + addAccessExpiresToDownloadsReport +); +``` + +This filter first adds the header to the CSV, then maps the data. + +With the plugin you've created, you should now be able to add data to the analytics table, the CSV generated on the server, and the CSV generated on the browser. diff --git a/docs/reporting/extending-woocommerce-admin-reports.md b/docs/reporting/extending-woocommerce-admin-reports.md new file mode 100644 index 00000000000..c6f1b075c59 --- /dev/null +++ b/docs/reporting/extending-woocommerce-admin-reports.md @@ -0,0 +1,302 @@ +# Extending WC-Admin reports + +## Introduction + +This document serves as a guide to extending WC-Admin Reports with a basic UI dropdown, added query parameters, and modification of SQL queries and resulting report data. This example will create a currency selector for viewing the Orders Report based on a specific currency. + +Code from this guide can be viewed in the [wc-admin code repository](https://github.com/woocommerce/woocommerce-admin/tree/main/docs/examples/extensions/sql-modification). + +## Getting started + +We'll be using a local installation of WordPress with WooCommerce and the development version of WC-Admin to take advantage of `create-wc-extension` as a way to easily scaffold a modern WordPress JavaScript environment for plugins. + +In your local install, clone and start WC-Admin if you haven't already. + +```sh +cd wp-content/plugins +git clone git@github.com:woocommerce/woocommerce-admin.git +cd woocommerce-admin +npm run build +``` + +Once thats working, we can setup the extension folder ready for JavaScript development. + +```sh +npm run create-wc-extension +``` + +After choosing a name, move into that folder and start webpack to watch and build files. + +```sh +cd ../ +npm install +npm start +``` + +Don't forget to head over to `/wp-admin/plugins.php` and activate your plugin. + +## Populating test data + +Next, set up some orders to have sample data. Using WooCommerce > Settings > Currency, I added three test orders in Mexican Peso, US Dollar, and New Zealand Dollar. + +After doing so, check out WC-Admin to make sure the orders are showing up by going to `/wp-admin/admin.php?page=wc-admin&period=today&path=%2Fanalytics%2Forders&compare=previous_year`. Note that without any modification currency figures show according to what I have currently in WooCommerce settings, which is New Zealand Dollar in this case. + +![screenshot of wp-admin showing processing orders](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-12.11.34-pm.png?w=851) + +We can confirm each order's currency by running the following query on the `wp_posts` table and joining `wp_postmeta` to gather currency meta values. Results show an order in NZD, USD, and MXN. This query is similar to the one we'll implement later in the guide to gather and display currency values. + +```sql +SELECT + ID, + post_name, + post_type, + currency_postmeta.meta_value AS currency +FROM `wp_posts` +JOIN wp_postmeta currency_postmeta ON wp_posts.ID = currency_postmeta.post_id +WHERE currency_postmeta.meta_key = '_order_currency' +ORDER BY wp_posts.post_date DESC +LIMIT 3 +``` + +![screenshot of resulting query](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-12.33.45-pm.png?w=756) + +## Add a UI dropdown + +In order to view reports in differing currencies, a filter or dropdown will be needed. We can add a basic filter to reports by adding a configuration object similar to [this one from the Orders Report](https://github.com/woocommerce/woocommerce-admin/blob/main/client/analytics/report/orders/config.js#L50-L62). + +First, we need to populate the client with data to render the dropdown. The best way to do this is to add data to the `wcSettings` global. This global can be useful for transferring static configuration data from PHP to the client. In the main PHP file, add currency settings to the Data Registry to populate `window.wcSettings.multiCurrency`. + +```php +function add_currency_settings() { + $currencies = array( + array( + 'label' => __( 'United States Dollar', 'dev-blog-example' ), + 'value' => 'USD', + ), + array( + 'label' => __( 'New Zealand Dollar', 'dev-blog-example' ), + 'value' => 'NZD', + ), + array( + 'label' => __( 'Mexican Peso', 'dev-blog-example' ), + 'value' => 'MXN', + ), + ); + + $data_registry = Automattic\WooCommerce\Blocks\Package::container()->get( + Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class + ); + + $data_registry->add( 'multiCurrency', $currencies ); +} + +add_action( 'init', 'add_currency_settings' ); +``` + +In the console, you can confirm the data has safely made its way to the client. + +![screnshot of console](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-1.11.50-pm.png?w=476) + +In `index.js` create the custom currency filter and add it the Orders Report. + +```js +import { addFilter } from "@wordpress/hooks"; +import { __ } from "@wordpress/i18n"; + +const addCurrencyFilters = (filters) => { + return [ + { + label: __("Currency", "dev-blog-example"), + staticParams: [], + param: "currency", + showFilters: () => true, + defaultValue: "USD", + filters: [...(wcSettings.multiCurrency || [])], + }, + ...filters, + ]; +}; + +addFilter( + "woocommerce_admin_orders_report_filters", + "dev-blog-example", + addCurrencyFilters +); +``` + +If we check out the Orders Report, we can see our new dropdown. Play around with it and you'll notice the currency query parameter gets added to the url. If you check out the Network tab, you'll also see this value included in requests for data used to populate the report. For example, see the requests to orders stats endpoint, `/wp-json/wc-analytics/reports/orders/stats`. Next we'll use that query parameter to adjust report results. + +![screenshot showing UI dropdown in wp-admin](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-1.16.44-pm.png?w=512) + +## Handle currency parameters on the server + +Now that our dropdown adds a `currency` query parameter to requests for data, the first thing we'll need to do is add the parameter as a query argument to the Orders Data Store and Orders Stats Data Store. Those data stores use query arguments for caching purposes, so by adding our parameter we can be sure a new database query will be performed when the parameter changes. Add the query argument in your main PHP file. + +```php +function apply_currency_arg( $args ) { + $currency = 'USD'; + + if ( isset( $_GET['currency'] ) ) { + $currency = sanitize_text_field( wp_unslash( $_GET['currency'] ) ); + } + + $args['currency'] = $currency; + + return $args; +} + +add_filter( 'woocommerce_analytics_orders_query_args', 'apply_currency_arg' ); +add_filter( 'woocommerce_analytics_orders_stats_query_args', 'apply_currency_arg' ); +``` + +Now that we're sure a new database query is performed on mutations of the `currency` query parameter, we can start adding SQL statements to the queries that gather data. + +Lets start by adding a JOIN for the orders table, orders stats, and orders chart. + +```php +function add_join_subquery( $clauses ) { + global $wpdb; + + $clauses[] = "JOIN {$wpdb->postmeta} currency_postmeta ON {$wpdb->prefix}wc_order_stats.order_id = currency_postmeta.post_id"; + + return $clauses; +} + +add_filter( 'woocommerce_analytics_clauses_join_orders_subquery', 'add_join_subquery' ); +add_filter( 'woocommerce_analytics_clauses_join_orders_stats_total', 'add_join_subquery' ); +add_filter( 'woocommerce_analytics_clauses_join_orders_stats_interval', 'add_join_subquery' ); +``` + +Next, add a WHERE clause + +```php +function add_where_subquery( $clauses ) { + $currency = 'USD'; + + if ( isset( $_GET['currency'] ) ) { + $currency = sanitize_text_field( wp_unslash( $_GET['currency'] ) ); + } + + $clauses[] = "AND currency_postmeta.meta_key = '_order_currency' AND currency_postmeta.meta_value = '{$currency}'"; + + return $clauses; +} + +add_filter( 'woocommerce_analytics_clauses_where_orders_subquery', 'add_where_subquery' ); +add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', 'add_where_subquery' ); +add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', 'add_where_subquery' ); +``` + +And finally, a SELECT clause. + +```php +function add_select_subquery( $clauses ) { + $clauses[] = ', currency_postmeta.meta_value AS currency'; + + return $clauses; +} + +add_filter( 'woocommerce_analytics_clauses_select_orders_subquery', 'add_select_subquery' ); +add_filter( 'woocommerce_analytics_clauses_select_orders_stats_total', 'add_select_subquery' ); +add_filter( 'woocommerce_analytics_clauses_select_orders_stats_interval', 'add_select_subquery' ); +``` + +Lets head back to the Orders Report and see if it works. You can manipulate the dropdown and see the relevant order reflected in the table. + +![screenshot of WooCommerce Orders tab in wp-admin showing the relevant order reflected in the table.](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-1.38.54-pm.png?w=585) + +## Finishing touches + +The orders table could use some customisation to reflect the selected currency. We can add a column to display the currency in `index.js`. The `reportTableData` argument is an object of headers, rows, and items, which are arrays of data. We'll need to add a new header and append the currency to each row's data array. + +```js +const addTableColumn = (reportTableData) => { + if ("orders" !== reportTableData.endpoint) { + return reportTableData; + } + + const newHeaders = [ + { + label: "Currency", + key: "currency", + }, + ...reportTableData.headers, + ]; + const newRows = reportTableData.rows.map((row, index) => { + const item = reportTableData.items.data[index]; + const newRow = [ + { + display: item.currency, + value: item.currency, + }, + ...row, + ]; + return newRow; + }); + + reportTableData.headers = newHeaders; + reportTableData.rows = newRows; + + return reportTableData; +}; + +addFilter("woocommerce_admin_report_table", "dev-blog-example", addTableColumn); +``` + +![screenshot of customized table](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-4.02.15-pm.png?w=861) + +While adding a column is certainly helpful, currency figures in the table and chart only reflect the store currency. + +![screenshot of report](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-4.03.42-pm.png?w=865) + +In order to change a Report's currency and number formatting, we can make use of the `woocommerce_admin_report_currency` JS hook. You can see the store's default sent to the client in `wcSettings.currency`, but we'll need to change these depending on the currency being viewed and designated by the query parameter `?currency=NZD`. + +![screenshot of currency settings](https://woocommerce.files.wordpress.com/2020/04/screen-shot-2020-04-03-at-11.18.42-am.png?w=238) + +First, lets create some configs in index.js. + +```js +const currencies = { + MXN: { + code: "MXN", + symbol: "$MXN", // For the sake of the example. + symbolPosition: "left", + thousandSeparator: ",", + decimalSeparator: ".", + precision: 2, + }, + NZD: { + code: "NZD", + symbol: "$NZ", + symbolPosition: "left", + thousandSeparator: ",", + decimalSeparator: ".", + precision: 2, + }, +}; +``` + +Finally, add our function to the hook which applies a config based on the currency query parameter. + +```js +const updateReportCurrencies = (config, { currency }) => { + if (currency && currencies[currency]) { + return currencies[currency]; + } + return config; +}; + +addFilter( + "woocommerce_admin_report_currency", + "dev-blog-example", + updateReportCurrencies +); +``` + +🎉 We can now view our Orders Report and see the currency reflected in monetary values throughout the report. + +![Screenshot of customized order report](https://woocommerce.files.wordpress.com/2020/04/screen-shot-2020-04-03-at-11.29.05-am.png?w=912) + +## Conclusion + +In this guide, we added a UI element to manipulate query parameters sent to the server and used those values to modify SQL statements which gather report data. In doing so, we established a way to highly customise WC-Admin reports. Hopefully this example illustrates how the platform can be tailored by extensions to bring a powerful experience to users. diff --git a/docs/reporting/readme.md b/docs/reporting/readme.md new file mode 100644 index 00000000000..1063addba3d --- /dev/null +++ b/docs/reporting/readme.md @@ -0,0 +1,5 @@ +# Reporting + +> ⚠️ **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! + +Understanding your WooCommerce store's performance is crucial. This section will provide insights into generating, understanding, and optimizing reports to make informed decisions about WooCommerce projects. diff --git a/docs/rest-api/readme.md b/docs/rest-api/readme.md new file mode 100644 index 00000000000..5033ef6331c --- /dev/null +++ b/docs/rest-api/readme.md @@ -0,0 +1,5 @@ +# REST API + +> ⚠️ **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! + +Harness the power of WooCommerce's REST API. This section will help you discover comprehensive documentation on endpoints, authentication, and best practices, aiding developers in integrating and manipulating WooCommerce functionalities programmatically. diff --git a/docs/security/readme.md b/docs/security/readme.md new file mode 100644 index 00000000000..3d864e93e08 --- /dev/null +++ b/docs/security/readme.md @@ -0,0 +1,5 @@ +# Security + +> ⚠️ **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! + +Security is paramount. This section will dive into best practices, guidelines, and insights to ensure your WooCommerce projects remain secure from threats. diff --git a/docs/security/reporting-security-issues.md b/docs/security/reporting-security-issues.md new file mode 100644 index 00000000000..7ee31b38c0e --- /dev/null +++ b/docs/security/reporting-security-issues.md @@ -0,0 +1,10 @@ +# Reporting security issues + +WooCommerce cares deeply about security and works hard to keep our merchants and their customers safe. + +You can find our security policy [over here](https://github.com/woocommerce/woocommerce/security/policy) and, if you believe you have discovered a vulnerability, we encourage you to follow it and submit your findings via [HackerOne](https://hackerone.com/automattic?type=team)—a trusted third party service that facilitates reporting of security issues. Please refer to the policy for more details, however some key points are as follows: + +- We operate a [bug bounty program](https://hackerone.com/automattic?type=team), so you can be rewarded for valid reports, but not everything is in scope. Please check the guidance before posting. +- We strongly encourage [responsible disclosure](https://www.hackerone.com/disclosure-guidelines). To better protect everyone, please use HackerOne and **do not** post your findings in a public forum. + +Thank you for being a responsible reporter! diff --git a/docs/security/security-best-practices.md b/docs/security/security-best-practices.md new file mode 100644 index 00000000000..3df5d766297 --- /dev/null +++ b/docs/security/security-best-practices.md @@ -0,0 +1,85 @@ +# WooCommerce security best practices + +## Introduction + +This guide covers the best practices for securing WooCommerce stores, including hardening WordPress, keeping plugins and themes up to date, implementing secure coding practices, and protecting user data. By following these recommendations, developers can build secure and resilient WooCommerce stores that protect both their business and their customers. + +## Audience + +This guide is intended for developers who are familiar with WordPress and WooCommerce and want to improve the security of their online stores. + +## Prerequisites + +To follow this guide, you should have: + +1. A basic understanding of WordPress and WooCommerce. +2. Access to a WordPress website with WooCommerce installed and activated. + +## Step 1 — Keep WordPress, WooCommerce, and plugins up to date + +Regularly updating WordPress, WooCommerce, and all installed plugins is crucial to maintaining a secure online store. Updates often include security patches that address vulnerabilities and help protect your store from attacks. To keep your WordPress and WooCommerce installations up to date: + +1. Enable automatic updates for WordPress core. +2. Regularly check for and install updates for WooCommerce and all plugins. + +## Step 2 — Choose secure plugins and themes + +The plugins and themes you use can have a significant impact on the security of your WooCommerce store. To ensure your store is secure: + +1. Install plugins and themes from reputable sources, such as the WordPress Plugin Directory and Theme Directory. +2. Regularly review and update the plugins and themes you use, removing any that are no longer maintained or have known security vulnerabilities. +3. Avoid using nulled or pirated plugins and themes, which may contain malicious code. + +## Step 3 — Implement secure coding practices + +Secure coding practices are essential for building a secure WooCommerce store. To implement secure coding practices: + +1. Follow the WordPress Coding Standards when developing custom themes or plugins. +2. Use prepared statements and parameterized queries to protect against SQL injection attacks. +3. Validate and sanitize user input to prevent cross-site scripting (XSS) attacks and other vulnerabilities. +4. Regularly review and update your custom code to address potential security vulnerabilities. + +## Step 4 — Harden WordPress security + +Hardening your WordPress installation can help protect your WooCommerce store from attacks. To harden your WordPress security: + +1. Use strong, unique passwords for all user accounts. +2. Limit login attempts and enable two-factor authentication (2FA) to protect against brute-force attacks. +3. Change the default "wp\_" table prefix in your WordPress database. +4. Disable XML-RPC and REST API access when not needed. +5. Keep file permissions secure and restrict access to sensitive files and directories. + +## Step 5 — Secure user data + +Protecting your customers' data is a critical aspect of securing your WooCommerce store. To secure user data: + +1. Use SSL certificates to encrypt data transmitted between your store and your customers. +2. Store customer data securely and limit access to sensitive information. +3. Comply with data protection regulations, such as the GDPR, to ensure you handle customer data responsibly. + +## Step 6 — Implement a security plugin + +Using a security plugin can help you monitor and protect your WooCommerce store from potential threats. To implement a security plugin: + +1. Choose a reputable security plugin, such as Wordfence, Sucuri, or iThemes Security. +2. Configure the plugin's settings to enable features like malware scanning, firewall protection, and login security. + +## Step 7 — Regularly monitor and audit your store's security + +Continuously monitor and audit your WooCommerce store's security to identify potential vulnerabilities and address them before they can be exploited. To monitor and audit your store's security: + +1. Use a security plugin to perform regular scans for malware and other security threats. +2. Monitor your site's activity logs to identify suspicious activity and potential security issues. +3. Perform regular security audits to evaluate your store's overall security and identify areas for improvement. + +## Step 8 — Create regular backups + +Backing up your WooCommerce store is essential for quickly recovering from security incidents, such as data loss or site compromise. To create regular backups: + +1. Choose a reliable backup plugin, such as UpdraftPlus, BackupBuddy, or Duplicator. +2. Configure the plugin to automatically create regular backups of your entire site, including the database, files, and media. +3. Store your backups securely off-site to ensure they are accessible in case of an emergency. + +## Conclusion + +By following these security best practices, you can build a secure and resilient WooCommerce store that protects both your business and your customers. Regularly monitoring, auditing, and updating your store's security measures will help ensure it remains protected as new threats and vulnerabilities emerge. diff --git a/docs/style-guide.md b/docs/style-guide.md new file mode 100644 index 00000000000..ad90ffe7007 --- /dev/null +++ b/docs/style-guide.md @@ -0,0 +1,118 @@ +# Technical documentation style guide + +This style guide is intended to provide guidelines for creating effective and user-friendly tutorials and how-to guides for WooCommerce technical documentation that will live in repo and be editable and iterative by open source contributors and WooCommerce teams. + +## Writing style + +### Language style + +- It’s important to use clear and concise language that is easy to understand. Use active voice and avoid using jargon or technical terms that may be unfamiliar to the user. The tone should be friendly and approachable, and should encourage the user to take action. + +- Articles are written in the 3rd-person voice. + Example: “Add an embed block to your page.” + +- Use American English for spelling and punctuation styles, or consider using a different word that doesn’t have a different spelling in other English variants. + +- Use sentence case (not title case) for docs titles and subheadings. + Example: “Introduction to the launch experience” rather than “Introduction to the Launch Experience.” + +- When referring to files or directories, the text formatting eliminates the need to include articles such as “the” and clarifying nouns such as “file” or “directory”. + Example: “files stored in ~~the~~ `/wp-content/uploads/` ~~directory~~” or “edit ~~the~~ `/config/config.yml` ~~file~~ with” + +### Writing tips + +- Our target audience has a range of roles and abilities. When creating a tutorial or how-to guide, it’s important to consider the intended audience. Are they beginners or advanced users? What is their technical background? Understanding the audience can help guide the level of detail and the choice of language used in the guide. + +- Use language understable even by readers with little technical knowledge and readers whose first language might not be English. + +- Consider that this might be the first WooCommerce documentation page the reader has seen. They may have arrived here via a Google search or another website. Give the reader enough context about the topic and link words and phrases to other relevant Docs articles as often as possible. + +- Consider notes and sections that provide insights, tips, or cautionary information to expand on topics with context that would be relevant to the reader. + +- When providing specific direction, best practices, or requirements, we recommend including a description of the potential consequences or impacts of not following the provided guidance. This can help seed additional search keywords into the document and provide better context when support links to the documentation. + +- Always write a conceptual, high-level introduction to the topic first, above any H2 subheading. + +### Tutorials + +Tutorials are comprehensive and designed to teach a new skill or concept. + +> You are the teacher, and you are responsible for what the student will do. Under your instruction, the student will execute a series of actions to achieve some end. +> +> [Divio Framework on Tutorial Writing](https://documentation.divio.com/tutorials/) + +### How-to guides + +How-to guides are focused and specific, providing instructions on how to accomplish a particular task or solve a particular problem. + +> How-to guides are wholly distinct from tutorials and must not be confused with them: +> +> - A tutorial is what you decide a beginner needs to know. +> - A how-to guide is an answer to a question that only a user with some experience could even formulate. +> +> [Divio Framework on How-to-Guide Writing](https://documentation.divio.com/how-to-guides/) + +## Formatting + +### Visual style + +- Use the H2 style for main headings to be programmatically listed in the articles table of contents. +- File names and directory paths should be stylized as code per the [HTML spec](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-code-element). + Example: `/wp-content/uploads/` +- References to a single directory should have a trailing slash (eg. “/” appended) to the name. + Example: “uploads/“ +- References to repositories should appear without forward slashes and not be formatted in any way. The first appearance of a repository in article content should link to the URL of the repository source whenever possible. + Example: “[woocommerce-blocks](https://github.com/woocommerce/woocommerce-blocks)” followed by “woocommerce-blocks” +- Inline references to functions and command line operations should be formatted as inline code. + Example: “Use `dig` to retrieve DNS information.” +- Functions should be styled with “Inline code” formatting and retain upper and lower case formatting as established from their source. + Example: `WP_Query` (not WP_query) + +### Visual aids + +Visual aids such as screenshots, diagrams, code snippets and videos can be very helpful in a how-to guide. They provide a visual reference that can help the user understand the instructions more easily. When including visual aids, be sure to label them clearly and provide a caption or description that explains what is being shown. + +### Acronyms + +Phrases that are more familiarly known in their acronym form can be used. The first time an acronym appears on any page, the full phrase must be included, followed by its acronym in parentheticals. + +Example: We’ve enhanced the querying functionality in WooCommerce with the introduction of High Performance Order Storage (HPOS). + +After that, the acronym can be used for the remainder of the page. + +When deciding if a term is common, consider the impact on translation and future internationalization (i18n) efforts. + +## Patterning + +### Article content + +When creating a how-to guide, it’s important to use a consistent and easy-to-follow format. Here is a suggested template for a software how-to guide: + +**Introduction**: Provide an overview of the task or feature that the guide covers. + +**Prerequisites**: List any prerequisites that are required to complete the task or use the feature. + +**Step-by-step instructions**: Provide detailed, step-by-step instructions for completing the task or using the feature. Use numbered steps and include screenshots or other visual aids where appropriate. + +**Troubleshooting**: Include a troubleshooting section that addresses common issues or errors that users may encounter. + +**Conclusion**: Summarize the key points covered in the guide and provide any additional resources or references that may be helpful. + +## Terminology + +### Reference to components and features + +- “**WordPress Admin dashboard**” should be presented in its complete form the first time it appears in an article, followed by its abbreviated form in parentheses (“WP Admin”). Thereafter the abbreviated form can be used for any reference to the WordPress Admin dashboard within the same article. +- When referring to the URL of the WordPress Admin dashboard, the shortened form `wp-admin` can be used. + +## Testing + +Before publishing a tutorial or guide, it’s important to test it thoroughly to ensure that the instructions are accurate and easy to follow. + +## Structure + +### Atomizing the docs + +Articles that cover too many topics in one place can make it difficult for users to find the information they are looking for. “Atomizing” the Docs refers to breaking down extensive articles into a group of smaller related articles. This group of articles often has a main “landing page” with a high-level overview of the group of articles, and the descriptive text provides links to the related articles that a user will find relevant. These groups of articles can be considered an information “molecule” formed by the smaller, atomized articles. + +Breaking out smaller chunks of content into their own articles makes it easier to link to specific topics rather than relying on links to more extensive articles with anchor tags. This more specific linking approach is helpful to our Support team but is also useful for cross-linking articles throughout the Docs site. diff --git a/docs/themes/README.md b/docs/theme-development/README.md similarity index 100% rename from docs/themes/README.md rename to docs/theme-development/README.md diff --git a/docs/themes/marketplace-guidelines.md b/docs/theme-development/marketplace-guidelines.md similarity index 100% rename from docs/themes/marketplace-guidelines.md rename to docs/theme-development/marketplace-guidelines.md diff --git a/docs/theme-development/theme-design-ux-guidelines.md b/docs/theme-development/theme-design-ux-guidelines.md new file mode 100644 index 00000000000..a87dca84b27 --- /dev/null +++ b/docs/theme-development/theme-design-ux-guidelines.md @@ -0,0 +1,85 @@ +# 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. + +We recommend you review the [UI best practices for WordPress](https://developer.wordpress.org/themes/advanced-topics/ui-best-practices/) to ensure your theme is aligned with the WordPress theme requirements. + +Make sure your theme fits one or more industries currently available in the [WooCommerce themes store](https://woocommerce.com/product-category/themes). It’s important that the theme offers enough originality and distinctiveness in its design, while keeping it familiar, in order to be distinguished from other themes on the WooCommerce theme store. Your theme should avoid copying existing themes on the WooCommerce theme store or other WordPress theme marketplaces. + +## Design + + +High-quality design is an important aspect of an online store, and that is driven by the theme design and content. The design of the theme should be simple, consistent, uncluttered, memorable, intuitive, efficient, and functional. When designing a new theme for WooCommerce special attention should be given to: + +### Layout + +The theme should be up to industry standards in terms of hierarchy, flow, content balance, and white space. + +Theme authors must ensure that store pages (shop, product page, categories, cart, checkout, profile page, etc) fit seamlessly with the theme since they are the central point of a WooCommerce theme. + +The Theme is expected to be fully functional and optimized to be accessed on common device types such as laptops, tablets, and smartphones. + +### Typography + +The theme should provide elegant and legible font pairings that promote a comfortable reading experience. + +Consistent and harmonious font sizes, line widths and spacing must be employed across all pages and device types. + +The theme typography must consist of a small number of typefaces that complement each other, generally no more than two. + +Proper capitalization is used, avoiding all caps (with the exception of some UI elements such as buttons, tabs, etc). + +### Iconography + +Icons used in the theme portray a direct meaning of the actions/situations they are representing and are used consistently regarding sizing positioning and color. + +### Color + +The theme must follow a harmonious and consistent color scheme across UI elements and all pages. The color scheme should consist of small number of colors that contain: + +- A primary/accent dominant color +- One or two secondary colors that complement the primary +- Neutral colors (white, black, gray) + +The color palette used in text and graphical UI components must be compliant with the [WCAG AA conformance level](https://www.w3.org/TR/WCAG20/#conformance) or above. + +### Patterns + +The theme must employ a consistent set of patterns that are used across pages, such as: + +- Navigation, sidebars, footer +- Content blocks (titles, paragraphs, lists, product details, reviews, image showcases, etc) +- Forms structure and elements (fields, drop-downs, buttons, etc) +- Tables +- Lists +- Notices + +## Accessibility + +The theme must meet the [Web Content Accessibility Guidelines](https://www.w3.org/TR/WCAG20/) (WCAG). Meeting 100% conformance with WCAG 2.0 is hard work; meet the AA level of conformance at a minimum. + +For more information on accessibility, check out the [WordPress accessibility quick start guide](https://make.wordpress.org/accessibility/handbook/best-practices/quick-start-guide/). + +## Customization + +Themes have to rely on the customizer for any type of initial set up. Specific onboarding flows are not permitted. + +Any customization supported by the theme, such as layout options, additional features, block options, etc, should be delivered in the customizer or on block settings for blocks that are included in the theme. + +Themes should not bundle or require the installation of additional plugins/extensions (or frameworks) that provide additional options or functionality. For more information on customisation, check out the [WordPress theme customization API](https://codex.wordpress.org/Theme_Customization_API)**.** + +On activation, themes shouldn’t override the WordPress theme activation flow by taking the user into other pages. + +## Branding + +The theme must not contain any branding or references to theme authors in locations that interfere with the normal operation of an online store. Theme authors can include links to their websites on the theme footer. Affiliate linking is not permitted. + +The interface should solely focus on the experience, the usage of notices, banners, large logos, or any promotional materials is not allowed in the admin interface. + +## Demos and sample content + +Upon submission theme authors must provide a way for the theme to be showcased and tested. The sample content/demo should refrain from using custom graphics/assets that will not be present in the deliverables to avoid merchant confusion and broken expectations (examples: using logos, illustrations). When creating a theme for a specific vertical theme authors should consider using sample content that aligns with the vertical. + +All imagery and text should be appropriate for all ages/family-friendly. The theme author should consider using imagery that is inclusive of ages, nationalities, etc. The theme should refrain from using imagery that looks like ‘stock photography’. + +The theme must be distributed and cleared of all the necessary licenses for assets such as images, fonts, icons, etc. diff --git a/docs/tutorials/adding-a-custom-field-to-variable-products.md b/docs/tutorials/adding-a-custom-field-to-variable-products.md new file mode 100644 index 00000000000..6d67ced0216 --- /dev/null +++ b/docs/tutorials/adding-a-custom-field-to-variable-products.md @@ -0,0 +1,253 @@ +# Adding a custom field to simple and variable products + +In this tutorial you will learn how to create a custom field for a product and show it in your store. Together we will set up the skeleton plugin, and learn about WP naming conventions and WooCommerce hooks. In the end, you will have a functioning plugin for adding a custom field. + +The [full plugin code](https://github.com/EdithAllison/woo-product-custom-fields) was written based on WordPress 6.2 and WooCommerce 7.6.0 + +## Prerequisites + +To do this tutorial you will need to have a WordPress install with the WooCommerce plugin activated, and you will need at least one [simple product set up](https://woocommerce.com/document/managing-products/), or you can [import the WooCommerce sample product range](https://woocommerce.com/document/importing-woocommerce-sample-data/). + +## Setting up the plugin + +To get started, let’s do the steps to [create a skeleton plugin](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/create-woo-extension). + +First, navigate to your wp-content/plugins folder, then run: + +```sh +npx @wordpress/create-block -t @woocommerce/create-woo-extension woo-product-fields +``` + +Then we navigate to our new folder and run the install and build: + +```sh +cd woo-product-fields +npm install # Install dependencies +npm run build # Build the javascript +``` + +WordPress has its own class file naming convention which doesn’t work with PSR-4 out of the box. To learn more about Naming Conventions see the [WP Handbook](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions). We will use the standard format of “class-my-classname.php” format, so let’s go to the composer.json file and change the autoload to: + +```json +"autoload": { + "classmap": ["includes/", "includes/admin/"] + }, +``` + +After saving run dump-autoload to generate the class map by running in the Terminal: + +```sh +composer dump-autoload -o +``` + +This generates a new vendor/composer/autoload_classmap.php file containing a list of all our classes in the /includes/ and /includes/admin/ folder. We will need to repeat this command when we add, delete or move class files. + +## WooCommerce Hooks + +Our aim is to create a new custom text field for WooCommerce products to save new stock information for display in the store. To do this, we need to modify the section of the Woo data in the admin area which holds the stock info. + +WooCommerce allows us to add our code to these sections through [hooks](https://developer.wordpress.org/plugins/hooks/), which are a standard WordPress method to extend code. In the “Inventory” section we have the following action hooks available to us: + +For our Woo extension, we’ll be appending our field right at the end with `woocommerce_product_options_inventory_product_data`. + +## Creating our class + +Let’s get started with creating a new class which will hold the code for the field. Add a new file with the name `class-product-fields.php` to the `/includes/admin/` folder. Within the class, we add our namespace, an abort if anyone tries to call the file directly and a \_\_construct method which calls the `hooks()` method: + +```php +hooks(); + } + + private function hooks() {} +} +``` + +Then in Terminal we run `composer dump-autoload -o` to regenerate the class map. Once that’s done, we add the class to our `setup.php` \_\_construct() function like so: + +```php +class Setup { + public function __construct() { + add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) ); + + new ProductFields(); + } +``` + +## Adding the custom field + +With the class set up and being called, we can create a function to add the custom field. WooCommerce has its own `woocommerce_wp_text_input( $args )` function which we can use here. `$args` is an array which allows us to set the text input data, and we will be using the global $product_object to access stored metadata. + +```php +public function add_field() { + global $product_object; + ?> +
+ '_new_stock_information', + 'label' => __( 'New Stock', 'woo_product_field' ), + 'description' => __( 'Information shown in store', 'woo_product_field' ), + 'desc_tip' => true, + 'value' => $product_object->get_meta( '_new_stock_information' ) + ) + ); ?> +
+ update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'] ) ); + $product->save_meta_data(); + } +} +``` + +This function checks if our new field is in the POST array. If yes, we create the product object, update our metadata and save the metadata. The `update_meta_data` function will either update an existing meta field or add a new one. And as we’re inserting into the database, we must [sanitize our field value](https://developer.wordpress.org/apis/security/sanitizing/). + +And to make it all work, we add the hooks: + +```php +private function hooks() { + add_action( 'woocommerce_product_options_inventory_product_data', array( $this, 'add_field' ) ); + add_action( 'woocommerce_process_product_meta', array( $this, 'save_field' ), 10, 2 ); +} +``` + +Now if we refresh our product screen, we can see our new field. + +If we add data and save the product, then the new meta data is inserted into the database. + +At this point you have a working extension that saves a custom field for a product as product meta. +Showing the field in the store +If we want to display the new field in our store, then we can do this with the `get_meta()` method of the Woo product class: `$product->get_meta( '\_new_stock_information' )` + +Let’s get started by creating a new file /includes/class-product.php. You may have noticed that this is outside the `/admin/` folder as this code will run in the front. So when we set up the class, we also adjust the namespace accordingly: + +```php +hooks(); + } + + private function hooks() { } +} +``` + +Again we run `composer dump-autoload -o` to update our class map. + +If you took a look at the extension setup you may have noticed that `/admin/setup.php` is only called if we’re within WP Admin. So to call our new class we’ll add it directly in `/woo-product-field.php`: + +```php +public function __construct() { + if ( is_admin() ) { + new Setup(); + } + new WooProductField\Product(); +} +``` + +For adding the field to the front we have several options. We could create a theme template, but if we are working with a WooCommerce-compatible theme and don’t need to make any other changes then a quick way is to use hooks. If we look into `/woocommerce/includes/wc-template-hooks.php` we can see all the existing actions for `woocommerce_single_product_summary` which controls the section at the top of the product page: + +For our extension, let's add the new stock information after the excerpt by using 21 as the priority: + +```php +private function hooks() { + add_action( 'woocommerce_single_product_summary', array( $this, 'add_stock_info' ), 21 ); +} +``` + +In our function we output the stock information with the [appropriate escape function](https://developer.wordpress.org/apis/security/escaping/), in this case, I’m suggesting to use `esc_html()` to force plain text. + +```php +public function add_stock_info() { + global $product; + ?> +

get_meta( '_new_stock_information' ) ); ?>

+ ID ); + + woocommerce_wp_text_input( + array( + 'id' => '\_new_stock_information' . '[' . $loop . ']', + 'label' => \_\_( 'New Stock Information', 'woo_product_field' ), + 'wrapper_class' => 'form-row form-row-full', + 'value' => $variation_product->get_meta( '\_new_stock_information' ) + ) + ); +} +``` + +For saving we use: + +```php +public function save_variation_field( $variation_id, $i ) { + if ( isset( $_POST['_new_stock_information'][$i] ) ) { + $variation_product = wc_get_product( $variation_id ); + $variation_product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'][$i] ) ); + $variation_product->save_meta_data(); + } +} +``` + +And we now have a new variation field that stores our new stock information. If you cannot see the new field, please make sure to enable “Manage Stock” for the variation by ticking the checkbox in the variation details. + +Displaying the variation in the front store works a bit differently for variable products as only some content on the page is updated when the customer makes a selection. This exceeds the scope of this tutorial, but if you are interested have a look at `/woocommerce/assets/js/frontend/add-to-cart-variation.js` to see how WooCommerce does it. + +## How to find hooks? + +Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`. diff --git a/docs/tutorials/adding-actions-and-filters.md b/docs/tutorials/adding-actions-and-filters.md new file mode 100644 index 00000000000..dfcd4490016 --- /dev/null +++ b/docs/tutorials/adding-actions-and-filters.md @@ -0,0 +1,191 @@ +Like many WordPress plugins, WooCommerce provides a range of actions and filters through which developers can extend and modify the platform. + +Often, when writing new code or revising existing code, there is a desire to add new hooks—but this should always be done with thoughtfulness and care. This document aims to provide high-level guidance on the matter. + +Practices we generally allow, support and encourage include: + +* [Using existing hooks (or other alternatives) in preference to adding new hooks](#prefer-existing-hooks-or-other-alternatives) +* [Adding lifecycle hooks](#adding-lifecycle-hooks) +* [Optional escape hooks](#escape-hooks) +* [Modifying the inputs and outputs of global rendering functions](#modifying-function-input-and-output-global-rendering-functions) +* [Preferring the passing of objects over IDs](#prefer-passing-objects-over-ids) + +On the flip side, there are several practices we discourage: + +* [Tying lifecycle hooks to methods of execution](#tying-lifecycle-hooks-to-methods-of-execution) +* [Using filters as feature flags](#using-filters-as-feature-flags) +* [Placing filter hooks inside templates and data stores](#placement-of-filter-hooks) +* [Enumeration values within hook names](#enumeration-values-inside-hook-names) + +Beyond those items, we generally otherwise adhere to WordPress coding standards. In regards to hooks, that specifically means following the: + +* [Documentation standards for hooks](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/php/#4-hooks-actions-and-filters) +* [Guidance on Dynamic hook names](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#interpolation-for-naming-dynamic-hooks) + +Please note that we provide example code throughout this guide to help illustrate some of the principles. However, to keep things concise, we usually omit unnecessary detail, including doc blocks (in practice, though, hooks should always be accompanied by doc blocks!). + +### Prefer existing hooks (or other alternatives) + +Hooks come with a long-term obligation: the last thing we want is to add a new hook that developers come to depend on, only to strip it away again. However, this can lead to difficulties when the time comes to refactor a piece of code that contains hooks, sometimes delaying meaningful change or limiting how easily we can implement a change without compromising on backward compatibility commitments. + +For those reasons, we always prefer that—wherever reasonable—an existing hook or alternative approach in preference to adding a new hook. + +### Adding lifecycle hooks + +Lifecycle hooks can be used to communicate that a lifecycle event is about to start, or that it has concluded. Examples of such events include: + +* Main product loop +* Dispatching emails +* Rendering a template +* Product or order status changes + +In general, lifecycle hooks: + +* Come in pairs (‘before’ and ‘after’) +* Are always actions, never filters +* The ‘before’ hook will generally always provide callbacks with the arguments array, if there is one +* The ‘after’ hook will generally also provide callbacks with the function’s return value, if there is one + +Note that lifecycle hooks primarily exist to let other systems observe, rather than to modify the result. Of course, this does not stop the function author from additionally providing a filter hook that serves this function. + +For example, noting that it is the process of fetching the promotions which we view as the “lifecycle event”, and not the function itself: + +```php +function woocommerce_get_current_promotions( ...$args ) { + /* Any initial prep, then first lifecycle hook... */ + do_action( 'woocommerce_before_get_current_promotions', $args ); + /* ...Do actual work, then final lifecycle hook... */ + do_action( 'woocommerce_after_get_current_promotions', $result, $args ); + /* ...Return the result, optionally via a filter... */ + return apply_filters( 'woocommerce_get_current_promotions', $result, $args ); +} +``` + +### Escape hooks + +In some cases, it may be appropriate to support short-circuiting of functions or methods. This is what we call an escape hook, and can be useful as a means of overriding code when a better way of doing so is not available. + +* Escape hooks are always filters +* They should always supply null as the initial filterable value +* If the value is changed to a non-null value, then the function should exit early by returning that new value + +For type safety, care should be taken to ensure that, if a function is short-circuited, the return type matches the function signature and/or return type stated in the function doc block. + +Example: + +```php +function get_product_metrics( $args ): array { + $pre = apply_filters( 'pre_woocommerce_get_product_metrics', null, $args ); + + if ( $pre !== null ) { + return (array) $pre; + } + + /* ...Default logic... */ + return $metrics; +} +``` + +### Modifying function input and output (global rendering functions) + +In the case of global rendering or formatting functions (so-called “template tags”), where it is not readily possible to implement better alternatives, it is permissible to add filters for both the function arguments and the function’s return value. + +This should be done sparingly, and only where necessary. Remember that while providing opportunities for other components to perform extensive customization, it can potentially derail other components which expect unmodified output. + +Example: + +```php +function woocommerce_format_sale_price( ...$args ): string { + /* Prep to fill in any missing $args values... */ + $args = (array) apply_filters( 'woocommerce_format_sale_price_args', $args ); + /* ...Actual work to determine the $price string... */ + return (string) apply_filters( 'woocommerce_format_sale_price', $price, $args ); +} +``` + +### Prefer passing objects over IDs + +Some actions or filters provide an object ID (such as a product ID) as their primary value, while others will provide the actual object itself (such as a product object). For consistency, it is preferred that objects be passed. + +Example: + +```php +function get_featured_product_for_current_customer( ) { + /* ...Logic to find the featured product for this customer… */ + + return apply_filters( + 'woocommerce_featured_product_for_current_customer', + $product, /* WC_Product */ + $customer + ); +} +``` + +### Tying lifecycle hooks to methods of execution + +There can sometimes be multiple paths leading to the same action. For instance, an order can be updated via the REST API, through the admin environment, or on the front end. It may additionally happen via ajax, or via a regular request. + +It is important however not to tie hooks for high-level processes to specific execution paths. For example, an action that fires when an order is created must not only be fired when this happens in the admin environment via an ajax request. + +Instead, prefer a more generic hook that passes context about the method of execution to the callback. + +Example of what we wish to avoid: + +```php +/** + * Pretend this function is only called following an ajax request + * (perhaps it is itself hooked in using a `wp_ajax_*` action). + */ +function on_ajax_order_creation() { + /* Avoid this! */ + do_action( 'woocommerce_on_order_creation' ); +} +``` + +### Using filters as feature flags + +It is sometimes tempting to use a filter as a sort of feature flag, that enables or disables a piece of functionality. This should be avoided! Prefer using an option: + +* Options persist in the database. +* Options are already filterable (ideal for a temporary override). + +Example of what we wish to avoid: + +```php +/* Avoid this */ +$super_products_enabled = (bool) apply_filters( 'woocommerce_super_products_are_enabled', true ); + +/* Prefer this */ +$super_products_enabled = get_option( 'woocommerce_super_products_are_enabled', 'no' ) === 'yes'; +``` + +### Placement of filter hooks + +Filters should not be placed inside templates—only actions. If it is important that a value used within a template be filterable, then the relevant logic should be moved to whichever function or method decides to load the template—the result being passed in as a template variable. + +It is also preferred that filter hooks not be placed inside data-store classes, as this can reduce the integrity of those components: since, by design, they are replaceable by custom implementations—the risk of accidentally breaking those custom stores is higher. + +### Enumeration values inside hook names + +Though there is a case for dynamic hook names (where part of the hook name is created using a variable), a good rule of thumb is to avoid this if the variable contains what might be considered an enumeration value. + +This might for instance include a case where an error code forms part of the hook name. + +Example (of what we wish to avoid): + +```php +if ( is_wp_error( $result ) ) { + /* Avoid this */ + $error_code = $result->get_error_code(); + do_action( "woocommerce_foo_bar_{$error_code}_problem", $intermediate_result ); + + /* Prefer this */ + do_action( 'woocommerce_foo_bar_problem', $result ); +} +``` + +The primary reason for avoiding this is that the more values there are in the enumeration set, the more filters developers have to include in their code. + +### Summary + +This document is a high-level guide to the inclusion and placement of hooks, not an exhaustive list. There will occasionally be exceptions, and there may be good rules and methodologies we are missing: if you have suggestions or ideas for improvement, please reach out! diff --git a/docs/tutorials/readme.md b/docs/tutorials/readme.md new file mode 100644 index 00000000000..3a4428d3c45 --- /dev/null +++ b/docs/tutorials/readme.md @@ -0,0 +1,5 @@ +# Tutorials + +> ⚠️ **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! + +This section will contain step-by-step guides and walkthroughs tailored for both novice and seasoned WooCommerce enthusiasts. Whether it's setting up a new feature or diving into complex customizations, our tutorials will cover a wide range of topics to help you achieve your goals. diff --git a/packages/js/components/changelog/add-39499 b/packages/js/components/changelog/add-39499 new file mode 100644 index 00000000000..886126605a4 --- /dev/null +++ b/packages/js/components/changelog/add-39499 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add Tooltip to each list item when need it diff --git a/packages/js/components/changelog/dev-38345_allow_select_multiple_items b/packages/js/components/changelog/dev-38345_allow_select_multiple_items new file mode 100644 index 00000000000..71cf1979e2f --- /dev/null +++ b/packages/js/components/changelog/dev-38345_allow_select_multiple_items @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Allow users to select multiple items from the media library while adding images #39741 diff --git a/packages/js/components/changelog/fix-38954_set_as_cover b/packages/js/components/changelog/fix-38954_set_as_cover new file mode 100644 index 00000000000..5befe01aea2 --- /dev/null +++ b/packages/js/components/changelog/fix-38954_set_as_cover @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update ImageGallery block toolbar, moving some options to an ellipsis dropdown menu. diff --git a/packages/js/components/changelog/fix-39810_categories_dropdown_visibility b/packages/js/components/changelog/fix-39810_categories_dropdown_visibility new file mode 100644 index 00000000000..301a25592c0 --- /dev/null +++ b/packages/js/components/changelog/fix-39810_categories_dropdown_visibility @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Categories dropdown display error #39810 diff --git a/packages/js/components/src/experimental-select-control/menu-item.tsx b/packages/js/components/src/experimental-select-control/menu-item.tsx index ef3dbbd465b..745ec825f70 100644 --- a/packages/js/components/src/experimental-select-control/menu-item.tsx +++ b/packages/js/components/src/experimental-select-control/menu-item.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import { Tooltip } from '@wordpress/components'; import { createElement, CSSProperties, ReactElement } from 'react'; /** @@ -15,6 +16,7 @@ export type MenuItemProps< ItemType > = { children: ReactElement | string; getItemProps: getItemPropsType< ItemType >; activeStyle?: CSSProperties; + tooltipText?: string; }; export const MenuItem = < ItemType, >( { @@ -24,14 +26,27 @@ export const MenuItem = < ItemType, >( { isActive, activeStyle = { backgroundColor: '#bde4ff' }, item, + tooltipText, }: MenuItemProps< ItemType > ) => { - return ( -
  • - { children } -
  • - ); + function renderListItem() { + return ( +
  • + { children } +
  • + ); + } + + if ( tooltipText ) { + return ( + + { renderListItem() } + + ); + } + + return renderListItem(); }; diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx index fe26480d94b..ee13ebfeb67 100644 --- a/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx +++ b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx @@ -104,6 +104,7 @@ export const SelectTreeMenu = ( { } ) } position={ position } + flip={ false } animate={ false } onFocusOutside={ ( event ) => { if ( isEventOutside( event ) ) { diff --git a/packages/js/components/src/image-gallery/image-gallery-item.scss b/packages/js/components/src/image-gallery/image-gallery-item.scss index 1ec6cc8462a..b1708371d22 100644 --- a/packages/js/components/src/image-gallery/image-gallery-item.scss +++ b/packages/js/components/src/image-gallery/image-gallery-item.scss @@ -1,6 +1,6 @@ .woocommerce-image-gallery__item { - height: 146px; - width: 146px; + height: 146px; + width: 146px; position: relative; &.is-toolbar-visible { @@ -9,40 +9,38 @@ } } - &:not(.is-toolbar-visible){ + &:not(.is-toolbar-visible) { img:hover { border: 1.5px solid #007cba; } } - img { + img { border: 1px solid $gray-200; border-radius: 3px; box-sizing: border-box; object-fit: cover; width: 100%; height: 146px; - } + } - .woocommerce-pill { - background: var(--wp-admin-theme-color); - border: 0; - color: $white; - padding-top: 2px; - padding-bottom: 2px; - position: absolute; - top: 10px; - left: 10px; - } + .woocommerce-pill { + background: var(--wp-admin-theme-color); + border: 0; + color: $white; + padding-top: 2px; + padding-bottom: 2px; + position: absolute; + top: 10px; + left: 10px; + } .components-toolbar-group { flex-wrap: inherit; - border-right: 1px solid #ccc; svg { width: 24px; margin-top: $gap-smallest; } } - } diff --git a/packages/js/components/src/image-gallery/image-gallery-toolbar-dropdown.scss b/packages/js/components/src/image-gallery/image-gallery-toolbar-dropdown.scss new file mode 100644 index 00000000000..d08e6ec7c7e --- /dev/null +++ b/packages/js/components/src/image-gallery/image-gallery-toolbar-dropdown.scss @@ -0,0 +1,6 @@ +// Hack to hide when the media modal is open +// Otherwise in Firefox the popover remains visible on top of the modal +// Changing the z-index of Popovers have wider implications. +.modal-open .woocommerce-image-gallery__toolbar-dropdown-popover { + display: none; +} diff --git a/packages/js/components/src/image-gallery/image-gallery-toolbar-dropdown.tsx b/packages/js/components/src/image-gallery/image-gallery-toolbar-dropdown.tsx new file mode 100644 index 00000000000..2c3627e09db --- /dev/null +++ b/packages/js/components/src/image-gallery/image-gallery-toolbar-dropdown.tsx @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { moreVertical } from '@wordpress/icons'; +import { + Children, + cloneElement, + createElement, + Fragment, + isValidElement, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { MediaItem, MediaUpload } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import { MediaUploadComponentType } from './types'; + +const POPOVER_PROPS = { + className: 'woocommerce-image-gallery__toolbar-dropdown-popover', + placement: 'bottom-start', +}; + +type ImageGalleryToolbarDropdownProps = { + onReplace: ( media: { id: number } & MediaItem ) => void; + onRemove: () => void; + canRemove?: boolean; + removeBlockLabel?: string; + MediaUploadComponent: MediaUploadComponentType; +}; + +export function ImageGalleryToolbarDropdown( { + children, + onReplace, + onRemove, + canRemove, + removeBlockLabel, + MediaUploadComponent = MediaUpload, + ...props +}: React.PropsWithChildren< ImageGalleryToolbarDropdownProps > ) { + return ( + + { ( { onClose } ) => ( + <> + + { + onReplace( media as MediaItem ); + onClose(); + } } + allowedTypes={ [ 'image' ] } + render={ ( { open } ) => ( + { + open(); + } } + > + { __( 'Replace', 'woocommerce' ) } + + ) } + /> + + { typeof children === 'function' + ? children( { onClose } ) + : Children.map( + children, + ( child ) => + isValidElement< { onClose: () => void } >( + child + ) && + cloneElement< { onClose: () => void } >( + child, + { onClose } + ) + ) } + { canRemove && ( + + { + onClose(); + onRemove(); + } } + > + { removeBlockLabel || + __( 'Remove', 'woocommerce' ) } + + + ) } + + ) } + + ); +} diff --git a/packages/js/components/src/image-gallery/image-gallery-toolbar.scss b/packages/js/components/src/image-gallery/image-gallery-toolbar.scss index fa9507fb86f..5417bc48489 100644 --- a/packages/js/components/src/image-gallery/image-gallery-toolbar.scss +++ b/packages/js/components/src/image-gallery/image-gallery-toolbar.scss @@ -1,4 +1,5 @@ .woocommerce-image-gallery__toolbar { + width: max-content; position: absolute; top: -58px; left: 50%; diff --git a/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx b/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx index 325b0c4f07c..a87155b2a66 100644 --- a/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx +++ b/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx @@ -2,17 +2,25 @@ * External dependencies */ import { createElement } from '@wordpress/element'; -import { Toolbar, ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { chevronRight, chevronLeft, trash } from '@wordpress/icons'; import { MediaItem, MediaUpload } from '@wordpress/media-utils'; import { __ } from '@wordpress/i18n'; +import { + Toolbar, + ToolbarButton, + ToolbarGroup, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore No types for this exist yet. + // eslint-disable-next-line @woocommerce/dependency-group + ToolbarItem, +} from '@wordpress/components'; /** * Internal dependencies */ -import { CoverImageIcon } from './icons'; import { SortableHandle } from '../sortable'; import { MediaUploadComponentType } from './types'; +import { ImageGalleryToolbarDropdown } from './image-gallery-toolbar-dropdown'; export type ImageGalleryToolbarProps = { childIndex: number; @@ -88,31 +96,60 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( { setAsCoverImage( childIndex ) } - icon={ CoverImageIcon } label={ __( 'Set as cover', 'woocommerce' ) } + > + { __( 'Set as cover', 'woocommerce' ) } + + + ) } + { isCoverItem && ( + + + replaceItem( childIndex, media as MediaItem ) + } + allowedTypes={ [ 'image' ] } + render={ ( { open } ) => ( + + { __( 'Replace', 'woocommerce' ) } + + ) } /> ) } - - - replaceItem( childIndex, media as MediaItem ) - } - allowedTypes={ [ 'image' ] } - render={ ( { open } ) => ( - - { __( 'Replace', 'woocommerce' ) } - - ) } - /> - - - removeItem( childIndex ) } - icon={ trash } - label={ __( 'Remove', 'woocommerce' ) } - /> - + { isCoverItem && ( + + removeItem( childIndex ) } + icon={ trash } + label={ __( 'Remove', 'woocommerce' ) } + /> + + ) } + { ! isCoverItem && ( + + + { ( toggleProps: { + 'data-toolbar-item': boolean; + ref: React.ForwardedRef< + typeof ImageGalleryToolbarDropdown + >; + } ) => ( + removeItem( childIndex ) } + onReplace={ ( media ) => + replaceItem( childIndex, media ) + } + MediaUploadComponent={ + MediaUploadComponent + } + { ...toggleProps } + /> + ) } + + + ) } ); diff --git a/packages/js/components/src/image-gallery/image-gallery.tsx b/packages/js/components/src/image-gallery/image-gallery.tsx index c33d87ae43d..f75f4c77847 100644 --- a/packages/js/components/src/image-gallery/image-gallery.tsx +++ b/packages/js/components/src/image-gallery/image-gallery.tsx @@ -129,6 +129,20 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( { event.relatedTarget as Element ).closest( '.media-modal, .components-modal__frame' + ) ) || + ( event.relatedTarget && + // Check if not a button within the toolbar is clicked, to prevent hiding the toolbar. + ( + event.relatedTarget as Element + ).closest( + '.woocommerce-image-gallery__toolbar' + ) ) || + ( event.relatedTarget && + // Prevent toolbar from hiding if the dropdown is clicked within the toolbar. + ( + event.relatedTarget as Element + ).closest( + '.woocommerce-image-gallery__toolbar-dropdown-popover' ) ) ) { return; diff --git a/packages/js/components/src/image-gallery/style.scss b/packages/js/components/src/image-gallery/style.scss index 950fbe6ca66..d72a84b7abb 100644 --- a/packages/js/components/src/image-gallery/style.scss +++ b/packages/js/components/src/image-gallery/style.scss @@ -1,3 +1,4 @@ -@import 'image-gallery.scss'; -@import 'image-gallery-item.scss'; -@import 'image-gallery-toolbar.scss'; +@import "image-gallery.scss"; +@import "image-gallery-item.scss"; +@import "image-gallery-toolbar.scss"; +@import "image-gallery-toolbar-dropdown.scss"; diff --git a/packages/js/components/src/media-uploader/media-uploader.tsx b/packages/js/components/src/media-uploader/media-uploader.tsx index 25861acbe45..c28b6ec29e9 100644 --- a/packages/js/components/src/media-uploader/media-uploader.tsx +++ b/packages/js/components/src/media-uploader/media-uploader.tsx @@ -24,7 +24,7 @@ type MediaUploaderProps = { MediaUploadComponent?: < T extends boolean = false >( props: MediaUpload.Props< T > ) => JSX.Element; - multipleSelect?: boolean; + multipleSelect?: boolean | string; onSelect?: ( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: ( { id: number } & { [ k: string ]: any } ) | MediaItem[] @@ -96,6 +96,7 @@ export const MediaUploader = ( { ( + + ) } + + ); +} diff --git a/packages/js/product-editor/src/blocks/notice/editor.scss b/packages/js/product-editor/src/blocks/notice/editor.scss new file mode 100644 index 00000000000..c1a7b474759 --- /dev/null +++ b/packages/js/product-editor/src/blocks/notice/editor.scss @@ -0,0 +1,7 @@ +.woocommerce-product-notice { + margin-top: $gap + $gap-large * 2; + button { + pointer-events: all; + cursor: pointer; + } +} diff --git a/packages/js/product-editor/src/blocks/notice/index.ts b/packages/js/product-editor/src/blocks/notice/index.ts new file mode 100644 index 00000000000..8118373f872 --- /dev/null +++ b/packages/js/product-editor/src/blocks/notice/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { BlockConfiguration } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { initBlock } from '../../utils/init-block'; +import blockConfiguration from './block.json'; +import { Edit, NoticeBlockAttributes } from './edit'; + +const { name, ...metadata } = + blockConfiguration as BlockConfiguration< NoticeBlockAttributes >; + +export { metadata, name }; + +export const settings: Partial< BlockConfiguration< NoticeBlockAttributes > > = + { + example: {}, + edit: Edit, + }; + +export function init() { + initBlock( { name, metadata, settings } ); +} diff --git a/packages/js/product-editor/src/blocks/style.scss b/packages/js/product-editor/src/blocks/style.scss index f70587d84b4..6ddb6950e68 100644 --- a/packages/js/product-editor/src/blocks/style.scss +++ b/packages/js/product-editor/src/blocks/style.scss @@ -5,6 +5,7 @@ @import 'inventory-email/editor.scss'; @import 'inventory-sku/editor.scss'; @import 'name/editor.scss'; +@import 'notice/editor.scss'; @import 'pricing/editor.scss'; @import 'regular-price/editor.scss'; @import 'sale-price/editor.scss'; diff --git a/packages/js/product-editor/src/blocks/variation-options/edit.tsx b/packages/js/product-editor/src/blocks/variation-options/edit.tsx index a1c63afcd27..481c0ea908f 100644 --- a/packages/js/product-editor/src/blocks/variation-options/edit.tsx +++ b/packages/js/product-editor/src/blocks/variation-options/edit.tsx @@ -139,6 +139,9 @@ export function Edit() { product_block_variable_options_notice_dismissed: 'yes', } ) } + disabledAttributeIds={ entityAttributes + .filter( ( attr ) => ! attr.variation ) + .map( ( attr ) => attr.id ) } uiStrings={ { notice, globalAttributeHelperMessage: '', diff --git a/packages/js/product-editor/src/blocks/variations/edit.tsx b/packages/js/product-editor/src/blocks/variations/edit.tsx index 7cc7853f655..9e6ba9a42f3 100644 --- a/packages/js/product-editor/src/blocks/variations/edit.tsx +++ b/packages/js/product-editor/src/blocks/variations/edit.tsx @@ -35,12 +35,7 @@ import { } from '../../hooks/use-product-attributes'; import { getAttributeId } from '../../components/attribute-control/utils'; import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper'; - -function hasAttributesUsedForVariations( - productAttributes: Product[ 'attributes' ] -) { - return productAttributes.some( ( { variation } ) => variation ); -} +import { hasAttributesUsedForVariations } from '../../utils'; function getFirstOptionFromEachAttribute( attributes: Product[ 'attributes' ] @@ -163,6 +158,9 @@ export function Edit( { selectedAttributeIds={ variationOptions.map( ( attr ) => attr.id ) } + disabledAttributeIds={ productAttributes + .filter( ( attr ) => ! attr.variation ) + .map( ( attr ) => attr.id ) } /> ) } diff --git a/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx b/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx index daa65dac132..4a937d83332 100644 --- a/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx +++ b/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx @@ -48,6 +48,7 @@ type AttributeControlProps = { onNoticeDismiss?: () => void; createNewAttributesAsGlobal?: boolean; useRemoveConfirmationModal?: boolean; + disabledAttributeIds?: number[]; uiStrings?: { notice?: string | React.ReactElement; emptyStateSubtitle?: string; @@ -59,7 +60,8 @@ type AttributeControlProps = { attributeRemoveLabel?: string; attributeRemoveConfirmationMessage?: string; attributeRemoveConfirmationModalMessage?: string; - globalAttributeHelperMessage: string; + globalAttributeHelperMessage?: string; + disabledAttributeMessage?: string; }; }; @@ -80,6 +82,7 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { uiStrings, createNewAttributesAsGlobal = false, useRemoveConfirmationModal = false, + disabledAttributeIds = [], } ) => { uiStrings = { newAttributeListItemLabel: __( 'Add new', 'woocommerce' ), @@ -279,6 +282,10 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { onAdd={ handleAdd } selectedAttributeIds={ value.map( ( attr ) => attr.id ) } createNewAttributesAsGlobal={ createNewAttributesAsGlobal } + disabledAttributeIds={ disabledAttributeIds } + disabledAttributeMessage={ + uiStrings.disabledAttributeMessage + } /> ) } @@ -292,22 +299,26 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { customAttributeHelperMessage={ uiStrings.customAttributeHelperMessage } - globalAttributeHelperMessage={ createInterpolateElement( - uiStrings.globalAttributeHelperMessage, - { - link: ( - - <> - - ), - } - ) } + globalAttributeHelperMessage={ + uiStrings.globalAttributeHelperMessage + ? createInterpolateElement( + uiStrings.globalAttributeHelperMessage, + { + link: ( + + <> + + ), + } + ) + : undefined + } onCancel={ () => { closeEditModal( currentAttribute ); onEditModalCancel( currentAttribute ); diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx index 5dc682b23f5..9c7a36adc51 100644 --- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx +++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx @@ -55,6 +55,8 @@ type NewAttributeModalProps = { onAdd: ( newCategories: EnhancedProductAttribute[] ) => void; selectedAttributeIds?: number[]; createNewAttributesAsGlobal?: boolean; + disabledAttributeIds?: number[]; + disabledAttributeMessage?: string; }; type AttributeForm = { @@ -88,6 +90,11 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { onAdd, selectedAttributeIds = [], createNewAttributesAsGlobal = false, + disabledAttributeIds = [], + disabledAttributeMessage = __( + 'Already used in Attributes', + 'woocommerce' + ), } ) => { const scrollAttributeIntoView = ( index: number ) => { setTimeout( () => { @@ -317,6 +324,12 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { createNewAttributesAsGlobal={ createNewAttributesAsGlobal } + disabledAttributeIds={ + disabledAttributeIds + } + disabledAttributeMessage={ + disabledAttributeMessage + } /> diff --git a/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.scss b/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.scss index cf1b23b60f0..0c83f6af4cd 100644 --- a/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.scss +++ b/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.scss @@ -8,3 +8,13 @@ margin-right: $gap-small; } } + +.woocommerce-experimental-select-control__popover-menu-container { + .woocommerce-experimental-select-control__menu-item[disabled] { + pointer-events: none; + color: $gray-600; + } + .disabled-element-wrapper { + cursor: not-allowed; + } +} diff --git a/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.tsx b/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.tsx index 3ff391d8d85..c47ed08e806 100644 --- a/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.tsx +++ b/packages/js/product-editor/src/components/attribute-input-field/attribute-input-field.tsx @@ -5,7 +5,7 @@ import { sprintf, __ } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; import { Spinner, Icon } from '@wordpress/components'; import { plus } from '@wordpress/icons'; -import { createElement } from '@wordpress/element'; +import { createElement, useMemo } from '@wordpress/element'; import { EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME, QueryProductAttribute, @@ -27,7 +27,9 @@ import { import { EnhancedProductAttribute } from '../../hooks/use-product-attributes'; import { TRACKS_SOURCE } from '../../constants'; -type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >; +type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' > & { + isDisabled?: boolean; +}; type AttributeInputFieldProps = { value?: EnhancedProductAttribute | null; @@ -39,6 +41,8 @@ type AttributeInputFieldProps = { label?: string; placeholder?: string; disabled?: boolean; + disabledAttributeIds?: number[]; + disabledAttributeMessage?: string; ignoredAttributeIds?: number[]; createNewAttributesAsGlobal?: boolean; }; @@ -53,6 +57,8 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { placeholder, label, disabled, + disabledAttributeIds = [], + disabledAttributeMessage, ignoredAttributeIds = [], createNewAttributesAsGlobal = false, } ) => { @@ -72,6 +78,18 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { }; } ); + const markedAttributes = useMemo( + function setDisabledAttribute() { + return ( + attributes?.map( ( attribute ) => ( { + ...attribute, + isDisabled: disabledAttributeIds.includes( attribute.id ), + } ) ) ?? [] + ); + }, + [ attributes, disabledAttributeIds ] + ); + const getFilteredItems = ( allItems: NarrowedQueryAttribute[], inputValue: string @@ -139,7 +157,7 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { return ( className="woocommerce-attribute-input-field" - items={ attributes || [] } + items={ markedAttributes || [] } label={ label || '' } disabled={ disabled } getFilteredItems={ getFilteredItems } @@ -179,7 +197,15 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { index={ index } isActive={ highlightedIndex === index } item={ item } - getItemProps={ getItemProps } + getItemProps={ ( options ) => ( { + ...getItemProps( options ), + disabled: item.isDisabled || undefined, + } ) } + tooltipText={ + item.isDisabled + ? disabledAttributeMessage + : undefined + } > { isNewAttributeListItem( item ) ? (
    diff --git a/packages/js/product-editor/src/components/attributes/attributes.tsx b/packages/js/product-editor/src/components/attributes/attributes.tsx index efc8c5315e5..50b5c83240c 100644 --- a/packages/js/product-editor/src/components/attributes/attributes.tsx +++ b/packages/js/product-editor/src/components/attributes/attributes.tsx @@ -3,6 +3,7 @@ */ import { createElement } from '@wordpress/element'; import { ProductAttribute } from '@woocommerce/data'; +import { __ } from '@wordpress/i18n'; import { recordEvent } from '@woocommerce/tracks'; /** @@ -31,6 +32,15 @@ export const Attributes: React.FC< AttributesProps > = ( { return ( !! attr.variation ) + .map( ( attr ) => attr.id ) } + uiStrings={ { + disabledAttributeMessage: __( + 'Already used in Variations', + 'woocommerce' + ), + } } onAdd={ () => { recordEvent( 'product_add_attributes_modal_add_button_click' ); } } diff --git a/packages/js/product-editor/src/components/notice/index.ts b/packages/js/product-editor/src/components/notice/index.ts new file mode 100644 index 00000000000..6351718c3ef --- /dev/null +++ b/packages/js/product-editor/src/components/notice/index.ts @@ -0,0 +1 @@ +export * from './notice'; diff --git a/packages/js/product-editor/src/components/notice/notice.tsx b/packages/js/product-editor/src/components/notice/notice.tsx new file mode 100644 index 00000000000..e3a6f213e57 --- /dev/null +++ b/packages/js/product-editor/src/components/notice/notice.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { ReactNode } from 'react'; +import { createElement } from '@wordpress/element'; +import classNames from 'classnames'; + +export type NoticeProps = { + title?: string; + content?: string; + className?: string; + type?: 'error-type' | 'success' | 'warning' | 'info'; + children?: ReactNode; +}; + +export function Notice( { + title = '', + content = '', + className, + type = 'info', + children, +}: NoticeProps ) { + return ( +
    + { title && ( +

    { title }

    + ) } + { content && ( +

    + { content } +

    + ) } +
    + { children } +
    +
    + ); +} diff --git a/packages/js/product-editor/src/components/notice/style.scss b/packages/js/product-editor/src/components/notice/style.scss new file mode 100644 index 00000000000..60857455cb0 --- /dev/null +++ b/packages/js/product-editor/src/components/notice/style.scss @@ -0,0 +1,13 @@ +.woocommerce-product-notice { + padding: $gap-small $gap; + p { + color: $gray-900; + letter-spacing: 0.24px; + } + button { + letter-spacing: 0.24px; + } + &.info { + background-color: #f0f6fc; + } +} diff --git a/packages/js/product-editor/src/components/variations-table/styles.scss b/packages/js/product-editor/src/components/variations-table/styles.scss index 2760045b9be..fa5479bf71d 100644 --- a/packages/js/product-editor/src/components/variations-table/styles.scss +++ b/packages/js/product-editor/src/components/variations-table/styles.scss @@ -49,6 +49,12 @@ align-items: center; justify-content: flex-end; + &--delete { + &.components-button.components-menu-item__button.is-link { + text-decoration: none; + } + } + .components-button { position: relative; color: var(--wp-admin-theme-color); @@ -63,8 +69,14 @@ } } - .components-button svg { - fill: none; + .components-button { + &.components-dropdown-menu__toggle.has-icon svg { + fill: inherit; + } + + svg { + fill: none; + } } .components-button--visible { diff --git a/packages/js/product-editor/src/components/variations-table/variations-table.tsx b/packages/js/product-editor/src/components/variations-table/variations-table.tsx index 4e50c9b80d5..5b467eda00d 100644 --- a/packages/js/product-editor/src/components/variations-table/variations-table.tsx +++ b/packages/js/product-editor/src/components/variations-table/variations-table.tsx @@ -1,22 +1,29 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { Button, Spinner, Tooltip } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { + Button, + DropdownMenu, + MenuGroup, + MenuItem, + Spinner, + Tooltip, +} from '@wordpress/components'; import { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, ProductVariation, } from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; +import { ListItem, Pagination, Sortable, Tag } from '@woocommerce/components'; import { - Link, - ListItem, - Pagination, - Sortable, - Tag, -} from '@woocommerce/components'; -import { getNewPath } from '@woocommerce/navigation'; -import { useContext, useState, createElement } from '@wordpress/element'; + useContext, + useState, + createElement, + Fragment, +} from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; +import { moreVertical } from '@wordpress/icons'; import classnames from 'classnames'; import truncate from 'lodash/truncate'; import { CurrencyContext } from '@woocommerce/currency'; @@ -34,6 +41,7 @@ import { getProductStockStatus, getProductStockStatusClass } from '../../utils'; import { DEFAULT_PER_PAGE_OPTION, PRODUCT_VARIATION_TITLE_LIMIT, + TRACKS_SOURCE, } from '../../constants'; const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' ); @@ -87,7 +95,7 @@ export function VariationsTable() { [ currentPage, perPage, productId ] ); - const { updateProductVariation } = useDispatch( + const { updateProductVariation, deleteProductVariation } = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME ); @@ -124,6 +132,29 @@ export function VariationsTable() { ); } + function handleDeleteVariationClick( variationId: number ) { + if ( isUpdating[ variationId ] ) return; + setIsUpdating( ( prevState ) => ( { + ...prevState, + [ variationId ]: true, + } ) ); + deleteProductVariation< Promise< ProductVariation > >( { + product_id: productId, + id: variationId, + } ) + .then( () => { + recordEvent( 'product_variations_delete', { + source: TRACKS_SOURCE, + } ); + } ) + .finally( () => + setIsUpdating( ( prevState ) => ( { + ...prevState, + [ variationId ]: false, + } ) ) + ); + } + return (
    { isLoading || @@ -266,17 +297,70 @@ export function VariationsTable() { ) } - - { __( 'Edit', 'woocommerce' ) } - + { ( { onClose } ) => ( + <> + + { + recordEvent( + 'product_variations_preview', + { + source: TRACKS_SOURCE, + } + ); + } } + > + { __( + 'Preview', + 'woocommerce' + ) } + + + + { + handleDeleteVariationClick( + variation.id + ); + onClose(); + } } + className="woocommerce-product-variations__actions--delete" + > + { __( + 'Delete', + 'woocommerce' + ) } + + + + ) } +
    ) ) } diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss index 0df9be94658..35084b6911b 100644 --- a/packages/js/product-editor/src/style.scss +++ b/packages/js/product-editor/src/style.scss @@ -15,6 +15,7 @@ @import "components/content-preview/style.scss"; @import "components/radio-field/style.scss"; +@import "components/notice/style.scss"; @import "components/iframe-editor/style.scss"; @import "components/details-categories-field/style.scss"; @import "components/details-categories-field/create-category-modal.scss"; diff --git a/packages/js/product-editor/src/utils/has-attributes-used-for-variations.ts b/packages/js/product-editor/src/utils/has-attributes-used-for-variations.ts new file mode 100644 index 00000000000..d7b19f17b86 --- /dev/null +++ b/packages/js/product-editor/src/utils/has-attributes-used-for-variations.ts @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { Product } from '@woocommerce/data'; + +/** + * Determine if any attribute in the list is used for variations. + * + * @param {Array} attributeList - List of product attributes. + * @return {boolean} True if any attribute is used for variations. + */ +export const hasAttributesUsedForVariations = ( + attributeList: Product[ 'attributes' ] +) => { + if ( ! Array.isArray( attributeList ) || ! attributeList.length ) { + return false; + } + return attributeList.some( ( { variation } ) => variation ); +}; diff --git a/packages/js/product-editor/src/utils/index.ts b/packages/js/product-editor/src/utils/index.ts index bf3e695d1c6..5b33ea896e9 100644 --- a/packages/js/product-editor/src/utils/index.ts +++ b/packages/js/product-editor/src/utils/index.ts @@ -18,6 +18,7 @@ import { getTruncatedProductVariationTitle, } from './get-product-variation-title'; import { preventLeavingProductForm } from './prevent-leaving-product-form'; +import { hasAttributesUsedForVariations } from './has-attributes-used-for-variations'; import { isValidEmail } from './validate-email'; export * from './create-ordered-children'; @@ -39,6 +40,7 @@ export { getProductTitle, getProductVariationTitle, getTruncatedProductVariationTitle, + hasAttributesUsedForVariations, isValidEmail, preventLeavingProductForm, PRODUCT_STATUS_LABELS, diff --git a/phpcs.xml b/phpcs.xml index 7f88fe86b89..fd355d33463 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -27,7 +27,7 @@ - + diff --git a/plugins/woo-ai/changelog/fix-woo-ai-settings-group b/plugins/woo-ai/changelog/fix-woo-ai-settings-group new file mode 100644 index 00000000000..8816613878c --- /dev/null +++ b/plugins/woo-ai/changelog/fix-woo-ai-settings-group @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Woo AI - Fix store branding settings retrieval for use with description generation. diff --git a/plugins/woo-ai/includes/class-woo-ai-settings.php b/plugins/woo-ai/includes/class-woo-ai-settings.php index ccf013738f3..d0c8850ebf7 100644 --- a/plugins/woo-ai/includes/class-woo-ai-settings.php +++ b/plugins/woo-ai/includes/class-woo-ai-settings.php @@ -29,6 +29,13 @@ class Woo_AI_Settings { */ protected $id = 'woo-ai-settings-tab'; + /** + * Tone of voice select options. + * + * @var array + */ + private $tone_of_voice_select_options; + /** * Constants used for naming of saved options in the database. */ @@ -52,6 +59,17 @@ class Woo_AI_Settings { public function __construct() { add_action( 'admin_enqueue_scripts', array( $this, 'add_woo_ai_settings_script' ) ); add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_woo_ai_settings' ), 10, 2 ); + add_filter( 'woocommerce_settings_groups', array( $this, 'add_woo_ai_settings_group' ) ); + add_filter( 'woocommerce_settings-woo-ai', array( $this, 'add_woo_ai_settings_group_settings' ) ); + + $this->tone_of_voice_select_options = array( + 'informal' => __( 'Relaxed and friendly.', 'woocommerce' ), + 'humorous' => __( 'Light-hearted and fun.', 'woocommerce' ), + 'neutral' => __( 'A balanced tone that uses casual expressions.', 'woocommerce' ), + 'youthful' => __( 'Friendly and cheeky tone.', 'woocommerce' ), + 'formal' => __( 'Direct yet respectful formal tone.', 'woocommerce' ), + 'motivational' => __( 'Passionate and inspiring.', 'woocommerce' ), + ); $this->add_sanitization_hooks(); } @@ -78,6 +96,49 @@ class Woo_AI_Settings { return wp_strip_all_tags( $raw_value ?? '' ); } + /** + * Adds settings which can be retrieved via the WooCommerce Settings API. + * + * @see https://github.com/woocommerce/woocommerce/wiki/Settings-API + * + * @param array $settings The original settings array. + * @return array The modified settings array. + */ + public function add_woo_ai_settings_group_settings( $settings ) { + $settings[] = array( + 'id' => 'tone-of-voice', + 'option_key' => self::TONE_OF_VOICE_OPTION_KEY, + 'label' => __( 'Storewide Tone of Voice', 'woocommerce' ), + 'description' => __( 'This controls the conversational tone that will be used when generating content.', 'woocommerce' ), + 'default' => 'neutral', + 'type' => 'select', + 'options' => $this->tone_of_voice_select_options, + ); + $settings[] = array( + 'id' => 'store-description', + 'option_key' => self::STORE_DESCRIPTION_OPTION_KEY, + 'label' => __( 'Store Description', 'woocommerce' ), + 'description' => __( 'This is a short description of your store which could be used to help generate content.', 'woocommerce' ), + 'type' => 'textarea', + ); + return $settings; + } + + /** + * Register our Woo AI plugin group to the WooCommerce Settings API. + * + * @param array $locations The original settings array. + * @return array The modified settings array. + */ + public function add_woo_ai_settings_group( $locations ) { + $locations[] = array( + 'id' => 'woo-ai', + 'label' => __( 'Woo AI', 'woocommerce' ), + 'description' => __( 'Settings for the Woo AI plugin.', 'woocommerce' ), + ); + return $locations; + } + /** * Add settings to the AI section. * @@ -121,14 +182,7 @@ class Woo_AI_Settings { 'name' => __( 'Tone of voice', 'woocommerce' ), 'desc' => __( 'Select the tone of voice for the AI', 'woocommerce' ), 'type' => 'select', - 'options' => array( - 'informal' => __( 'Relaxed and friendly.', 'woocommerce' ), - 'humorous' => __( 'Light-hearted and fun.', 'woocommerce' ), - 'neutral' => __( 'A balanced tone that uses casual expressions.', 'woocommerce' ), - 'youthful' => __( 'Friendly and cheeky tone.', 'woocommerce' ), - 'formal' => __( 'Direct yet respectful formal tone.', 'woocommerce' ), - 'motivational' => __( 'Passionate and inspiring.', 'woocommerce' ), - ), + 'options' => $this->tone_of_voice_select_options, 'css' => 'min-width:300px;', ); diff --git a/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx b/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx index 9691ee441cd..fee71d85f7d 100644 --- a/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx @@ -14,6 +14,7 @@ import { PluginsLearnMoreLinkClicked, PluginsInstallationCompletedWithErrorsEvent, PluginsInstallationCompletedEvent, + PluginsInstallationRequestedEvent, } from '..'; import { POSSIBLY_DEFAULT_STORE_NAMES } from '../pages/BusinessInfo'; import { @@ -97,6 +98,20 @@ const recordTracksBusinessInfoCompleted = ( } ); }; +const recordTracksPluginsInstallationRequest = ( + _context: CoreProfilerStateMachineContext, + event: Extract< + PluginsInstallationRequestedEvent, + { type: 'PLUGINS_INSTALLATION_REQUESTED' } + > +) => { + recordEvent( 'coreprofiler_store_extensions_continue', { + shown: event.payload.pluginsShown || [], + selected: event.payload.pluginsSelected || [], + unselected: event.payload.pluginsUnselected || [], + } ); +}; + const recordTracksPluginsLearnMoreLinkClicked = ( _context: unknown, _event: PluginsLearnMoreLinkClicked, @@ -164,4 +179,5 @@ export default { recordTracksPluginsLearnMoreLinkClicked, recordFailedPluginInstallations, recordSuccessfulPluginInstallation, + recordTracksPluginsInstallationRequest, }; diff --git a/plugins/woocommerce-admin/client/core-profiler/components/loader/Loader.tsx b/plugins/woocommerce-admin/client/core-profiler/components/loader/Loader.tsx new file mode 100644 index 00000000000..b95ecd7dceb --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/loader/Loader.tsx @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import React from 'react'; +import { Loader } from '@woocommerce/onboarding'; +/** + * Internal dependencies + */ +import { CoreProfilerStateMachineContext } from '../..'; +import { getLoaderStageMeta } from '../../utils/get-loader-stage-meta'; + +import './loader.scss'; + +export type Stage = { + title: string; + image?: string | JSX.Element; + paragraphs: Array< { + label: string; + text: string; + duration?: number; + element?: JSX.Element; + } >; +}; + +export type Stages = Array< Stage >; +export type LoaderContextProps = Pick< + CoreProfilerStateMachineContext, + 'loader' +>; + +export const CoreProfilerLoader = ( { + context, +}: { + context: LoaderContextProps; +} ) => { + const stages = getLoaderStageMeta( context.loader.useStages ?? 'default' ); + const currentStage = stages[ context.loader.stageIndex ?? 0 ]; + + return ( + + + + { currentStage.image } + + + { currentStage.title } + + + { currentStage.paragraphs.map( ( paragraph, index ) => ( + + { paragraph?.label } + { paragraph?.text } + { paragraph?.element } + + ) ) } + + + + ); +}; diff --git a/plugins/woocommerce-admin/client/core-profiler/components/loader/loader.scss b/plugins/woocommerce-admin/client/core-profiler/components/loader/loader.scss new file mode 100644 index 00000000000..f86b218f1e2 --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/components/loader/loader.scss @@ -0,0 +1,8 @@ +// Loader page +.woocommerce-onboarding-loader { + .loader-hearticon { + position: relative; + top: 2px; + left: 2px; + } +} diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 1029ca4a0f9..e80e99e09e9 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -53,7 +53,7 @@ import { } from './pages/BusinessInfo'; import { BusinessLocation } from './pages/BusinessLocation'; import { getCountryStateOptions } from './services/country'; -import { Loader } from './pages/Loader'; +import { CoreProfilerLoader } from './components/loader/Loader'; import { Plugins } from './pages/Plugins'; import { getPluginSlug, useFullScreen } from '~/utils'; import './style.scss'; @@ -108,7 +108,9 @@ export type BusinessLocationEvent = { export type PluginsInstallationRequestedEvent = { type: 'PLUGINS_INSTALLATION_REQUESTED'; payload: { - plugins: CoreProfilerStateMachineContext[ 'pluginsSelected' ]; + pluginsShown: string[]; + pluginsSelected: string[]; + pluginsUnselected: string[]; }; }; @@ -342,7 +344,19 @@ const redirectToJetpackAuthPage = ( _context: CoreProfilerStateMachineContext, event: { data: { url: string } } ) => { - window.location.href = event.data.url + '&installed_ext_success=1'; + const url = new URL( event.data.url ); + url.searchParams.set( 'installed_ext_success', '1' ); + const selectedPlugin = _context.pluginsSelected.find( + ( plugin ) => plugin === 'jetpack' || plugin === 'jetpack-boost' + ); + + if ( selectedPlugin ) { + const pluginName = + selectedPlugin === 'jetpack' ? 'jetpack-ai' : 'jetpack-boost'; + url.searchParams.set( 'plugin_name', pluginName ); + } + + window.location.href = url.toString(); }; const updateTrackingOption = async ( @@ -551,7 +565,7 @@ const updateQueryStep: CoreProfilerMachineAssign = ( const assignPluginsSelected = assign( { pluginsSelected: ( _context, event: PluginsInstallationRequestedEvent ) => { - return event.payload.plugins.map( getPluginSlug ); + return event.payload.pluginsSelected.map( getPluginSlug ); }, } ); @@ -1109,7 +1123,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { }, }, meta: { - component: Loader, + component: CoreProfilerLoader, }, }, }, @@ -1174,7 +1188,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { ], }, meta: { - component: Loader, + component: CoreProfilerLoader, }, }, plugins: { @@ -1202,7 +1216,10 @@ export const coreProfilerStateMachineDefinition = createMachine( { }, PLUGINS_INSTALLATION_REQUESTED: { target: 'installPlugins', - actions: [ 'assignPluginsSelected' ], + actions: [ + 'assignPluginsSelected', + 'recordTracksPluginsInstallationRequest', + ], }, }, meta: { @@ -1233,7 +1250,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { ], }, meta: { - component: Loader, + component: CoreProfilerLoader, progress: 100, }, }, @@ -1255,7 +1272,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { ], }, meta: { - component: Loader, + component: CoreProfilerLoader, progress: 100, }, }, @@ -1286,7 +1303,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { }, }, meta: { - component: Loader, + component: CoreProfilerLoader, progress: 100, }, }, @@ -1381,7 +1398,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { }, }, meta: { - component: Loader, + component: CoreProfilerLoader, }, }, }, diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/Loader.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/Loader.tsx deleted file mode 100644 index 312869fd3fd..00000000000 --- a/plugins/woocommerce-admin/client/core-profiler/pages/Loader.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * External dependencies - */ -import classNames from 'classnames'; -import { useState, useEffect } from '@wordpress/element'; -/** - * Internal dependencies - */ -import { CoreProfilerStateMachineContext } from '..'; -import ProgressBar from '../components/progress-bar/progress-bar'; -import { getLoaderStageMeta } from '../utils/get-loader-stage-meta'; - -export type Stage = { - title: string; - image?: string | JSX.Element; - paragraphs: Array< { - label: string; - text: string; - duration?: number; - element?: JSX.Element; - } >; -}; - -export type Stages = Array< Stage >; -export type LoaderContextProps = Pick< - CoreProfilerStateMachineContext, - 'loader' ->; - -export const Loader = ( { context }: { context: LoaderContextProps } ) => { - const stages = getLoaderStageMeta( context.loader.useStages ?? 'default' ); - const currentStage = stages[ context.loader.stageIndex ?? 0 ]; - const [ currentParagraph, setCurrentParagraph ] = useState( 0 ); - - useEffect( () => { - const interval = setInterval( () => { - setCurrentParagraph( ( _currentParagraph ) => - currentStage.paragraphs[ _currentParagraph + 1 ] - ? _currentParagraph + 1 - : 0 - ); - }, currentStage.paragraphs[ currentParagraph ]?.duration ?? 3000 ); - - return () => clearInterval( interval ); - }, [ currentParagraph, currentStage.paragraphs ] ); - - return ( -
    -
    - { currentStage.image && currentStage.image } - -

    - { currentStage.title } -

    - -

    - - { currentStage.paragraphs[ currentParagraph ]?.label }{ ' ' } - - { currentStage.paragraphs[ currentParagraph ]?.text } - { currentStage.paragraphs[ currentParagraph ]?.element } -

    -
    -
    - ); -}; diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx index 375beafae8d..c67197cabd9 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx @@ -72,13 +72,33 @@ export const Plugins = ( { type: 'PLUGINS_PAGE_SKIPPED', } ); }; + const submitInstallationRequest = () => { + const selectedPluginSlugs = selectedPlugins.map( ( plugin ) => + plugin.key.replace( ':alt', '' ) + ); + + const pluginsShown: string[] = []; + const pluginsUnselected: string[] = []; + + context.pluginsAvailable.forEach( ( plugin ) => { + const pluginSlug = plugin.key.replace( ':alt', '' ); + pluginsShown.push( pluginSlug ); + + if ( + ! plugin.is_activated && + ! selectedPluginSlugs.includes( pluginSlug ) + ) { + pluginsUnselected.push( pluginSlug ); + } + } ); + return sendEvent( { type: 'PLUGINS_INSTALLATION_REQUESTED', payload: { - plugins: selectedPlugins.map( ( plugin ) => - plugin.key.replace( ':alt', '' ) - ), + pluginsShown, + pluginsSelected: selectedPluginSlugs, + pluginsUnselected, }, } ); }; diff --git a/plugins/woocommerce-admin/client/core-profiler/stories/Loader.tsx b/plugins/woocommerce-admin/client/core-profiler/stories/Loader.tsx index d12fb02adec..46de33c1c81 100644 --- a/plugins/woocommerce-admin/client/core-profiler/stories/Loader.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/stories/Loader.tsx @@ -1,23 +1,25 @@ /** * Internal dependencies */ -import { Loader } from '../pages/Loader'; +import { CoreProfilerLoader } from '../components/loader/Loader'; import { WithSetupWizardLayout } from './WithSetupWizardLayout'; import '../style.scss'; export const Short = () => ( - ); export const Plugins = () => ( - + ); export default { title: 'WooCommerce Admin/Application/Core Profiler/Loader', - component: Loader, + component: CoreProfilerLoader, decorators: [ WithSetupWizardLayout ], }; diff --git a/plugins/woocommerce-admin/client/core-profiler/style.scss b/plugins/woocommerce-admin/client/core-profiler/style.scss index 85de10bc0f8..59bff6ff47e 100644 --- a/plugins/woocommerce-admin/client/core-profiler/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/style.scss @@ -273,63 +273,6 @@ } } -// Loader page -.woocommerce-profiler-loader { - min-height: 100vh; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - @include breakpoint( '<782px' ) { - padding: 0 20px; - } - - .loader-hearticon { - position: relative; - top: 2px; - left: 2px; - } - - h1 { - padding: 0; - margin: 58px 0 0 0; - font-size: 28px; - font-weight: 500; - @include breakpoint( '<782px' ) { - font-size: 20px; - } - } - .woocommerce-profiler-loader-wrapper { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - max-width: 520px; - } - .woocommerce-profiler-progress-bar { - width: 520px; - margin: 16px 0 16px 0; - @include breakpoint( '<782px' ) { - width: 100%; - } - } - .woocommerce-profiler-progress-bar__container { - height: 4px; - } - - .woocommerce-profiler-loader__paragraph { - font-size: 16px; - line-height: 24px; - color: #2f2f2f; - opacity: 0.8; - text-align: center; - @include breakpoint( '<782px' ) { - font-size: 14px; - line-height: 20px; - } - } -} - // User profile page .woocommerce-profiler-user-profile { .woocommerce-profiler-user-profile__content { diff --git a/plugins/woocommerce-admin/client/core-profiler/types.tsx b/plugins/woocommerce-admin/client/core-profiler/types.tsx index 335842d5b6f..75446d3778b 100644 --- a/plugins/woocommerce-admin/client/core-profiler/types.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/types.tsx @@ -6,7 +6,7 @@ import { CoreProfilerStateMachineContext } from '.'; export type ComponentMeta = { /** React component that is rendered when state matches the location this meta key is defined */ component: ( arg0: ComponentProps ) => JSX.Element; - /** number between 0 - 100 */ + /** Number between 0 - 100 */ progress: number; }; diff --git a/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx b/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx index d312814bc3f..996d4c22f96 100644 --- a/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx @@ -12,7 +12,7 @@ import LayoutImage from '../assets/images/loader-layout.svg'; import OpeningTheDoorsImage from '../assets/images/loader-openingthedoors.svg'; import Hearticon from '../assets/images/loader-hearticon.svg'; -import { Stages } from '../pages/Loader'; +import { Stages } from '../components/loader/Loader'; const LightbulbStage = { title: __( 'Turning on the lights', 'woocommerce' ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx new file mode 100644 index 00000000000..232ea95e3eb --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx @@ -0,0 +1 @@ +export type events = { type: 'FINISH_CUSTOMIZATION' }; diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/index.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/index.tsx new file mode 100644 index 00000000000..d7d96a1dd10 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/index.tsx @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import { CustomizeStoreComponent } from '../types'; + +export type events = { type: 'THEME_SUGGESTED' }; +export const DesignWithAi: CustomizeStoreComponent = ( { sendEvent } ) => { + return ( + <> +

    Design with AI

    + + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/index.tsx b/plugins/woocommerce-admin/client/customize-store/index.tsx new file mode 100644 index 00000000000..26b102faab1 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/index.tsx @@ -0,0 +1,193 @@ +/** + * External dependencies + */ +import { createMachine } from 'xstate'; +import { useEffect, useMemo, useState } from '@wordpress/element'; +import { useMachine, useSelector } from '@xstate/react'; + +/** + * Internal dependencies + */ +import { useFullScreen } from '~/utils'; +import { + Intro, + events as introEvents, + services as introServices, + actions as introActions, +} from './intro'; +import { DesignWithAi, events as designWithAiEvents } from './design-with-ai'; +import { events as assemblerHubEvents } from './assembler-hub'; +import { findComponentMeta } from '~/utils/xstate/find-component'; +import { + CustomizeStoreComponentMeta, + CustomizeStoreComponent, + customizeStoreStateMachineContext, +} from './types'; +import { ThemeCard } from './intro/theme-cards'; + +export type customizeStoreStateMachineEvents = + | introEvents + | designWithAiEvents + | assemblerHubEvents; + +export const customizeStoreStateMachineServices = { + ...introServices, +}; + +export const customizeStoreStateMachineActions = { + ...introActions, +}; +export const customizeStoreStateMachineDefinition = createMachine( { + id: 'customizeStore', + initial: 'intro', + predictableActionArguments: true, + preserveActionOrder: true, + schema: { + context: {} as customizeStoreStateMachineContext, + events: {} as customizeStoreStateMachineEvents, + services: {} as { + fetchThemeCards: { data: ThemeCard[] }; + }, + }, + context: { + intro: { + themeCards: [] as ThemeCard[], + activeTheme: '', + }, + } as customizeStoreStateMachineContext, + states: { + intro: { + id: 'intro', + initial: 'preIntro', + states: { + preIntro: { + invoke: { + src: 'fetchThemeCards', + onDone: { + target: 'intro', + actions: [ 'assignThemeCards' ], + }, + }, + }, + intro: { + meta: { + component: Intro, + }, + }, + }, + on: { + DESIGN_WITH_AI: { + target: 'designWithAi', + }, + SELECTED_ACTIVE_THEME: { + target: 'assemblerHub', + }, + CLICKED_ON_BREADCRUMB: { + target: 'backToHomescreen', + }, + SELECTED_NEW_THEME: { + target: '? Appearance Task ?', + }, + SELECTED_BROWSE_ALL_THEMES: { + target: '? Appearance Task ?', + }, + }, + }, + designWithAi: { + initial: 'preDesignWithAi', + states: { + preDesignWithAi: { + always: { + target: 'designWithAi', + }, + }, + designWithAi: { + meta: { + component: DesignWithAi, + }, + }, + }, + on: { + THEME_SUGGESTED: { + target: 'assemblerHub', + }, + }, + }, + assemblerHub: { + on: { + FINISH_CUSTOMIZATION: { + target: 'backToHomescreen', + }, + }, + }, + backToHomescreen: {}, + '? Appearance Task ?': {}, + }, +} ); + +export const CustomizeStoreController = ( { + actionOverrides, + servicesOverrides, +}: { + actionOverrides: Partial< typeof customizeStoreStateMachineActions >; + servicesOverrides: Partial< typeof customizeStoreStateMachineServices >; +} ) => { + useFullScreen( [ 'woocommerce-customize-store' ] ); + + const augmentedStateMachine = useMemo( () => { + return customizeStoreStateMachineDefinition.withConfig( { + services: { + ...customizeStoreStateMachineServices, + ...servicesOverrides, + }, + actions: { + ...customizeStoreStateMachineActions, + ...actionOverrides, + }, + guards: {}, + } ); + }, [ actionOverrides, servicesOverrides ] ); + + const [ state, send, service ] = useMachine( augmentedStateMachine, { + devTools: process.env.NODE_ENV === 'development', + } ); + + // eslint-disable-next-line react-hooks/exhaustive-deps -- false positive due to function name match, this isn't from react std lib + const currentNodeMeta = useSelector( service, ( currentState ) => + findComponentMeta< CustomizeStoreComponentMeta >( + currentState?.meta ?? undefined + ) + ); + + const [ CurrentComponent, setCurrentComponent ] = + useState< CustomizeStoreComponent | null >( null ); + useEffect( () => { + if ( currentNodeMeta?.component ) { + setCurrentComponent( () => currentNodeMeta?.component ); + } + }, [ CurrentComponent, currentNodeMeta?.component ] ); + + const currentNodeCssLabel = + state.value instanceof Object + ? Object.keys( state.value )[ 0 ] + : state.value; + + return ( + <> +
    + { CurrentComponent ? ( + + ) : ( +
    + ) } +
    + + ); +}; + +export default CustomizeStoreController; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/actions.ts b/plugins/woocommerce-admin/client/customize-store/intro/actions.ts new file mode 100644 index 00000000000..2330f5a3b48 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/intro/actions.ts @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { assign, DoneInvokeEvent } from 'xstate'; + +/** + * Internal dependencies + */ +import { customizeStoreStateMachineEvents } from '..'; + +/** + * Internal dependencies + */ +import { customizeStoreStateMachineContext } from '../types'; +import { ThemeCard } from './theme-cards'; + +export const assignThemeCards = assign< + customizeStoreStateMachineContext, + customizeStoreStateMachineEvents // this is actually the wrong type for the event but I still don't know how to type this properly +>( { + intro: ( context, event ) => { + const themeCards = ( event as DoneInvokeEvent< ThemeCard[] > ).data; // type coercion workaround for now + return { ...context.intro, themeCards }; + }, +} ); diff --git a/plugins/woocommerce-admin/client/customize-store/intro/index.tsx b/plugins/woocommerce-admin/client/customize-store/intro/index.tsx new file mode 100644 index 00000000000..be1f292d042 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/intro/index.tsx @@ -0,0 +1,42 @@ +/** + * Internal dependencies + */ +import { CustomizeStoreComponent } from '../types'; + +export type events = + | { type: 'DESIGN_WITH_AI' } + | { type: 'CLICKED_ON_BREADCRUMB' } + | { type: 'SELECTED_BROWSE_ALL_THEMES' } + | { type: 'SELECTED_ACTIVE_THEME' } + | { type: 'SELECTED_NEW_THEME'; payload: { theme: string } }; + +export * as actions from './actions'; +export * as services from './services'; + +export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => { + const { + intro: { themeCards, activeTheme }, + } = context; + return ( + <> +

    Intro

    +
    Active theme: { activeTheme }
    + { themeCards?.map( ( themeCard ) => ( + + ) ) } + + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/services.ts b/plugins/woocommerce-admin/client/customize-store/intro/services.ts new file mode 100644 index 00000000000..37c282ab5a4 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/intro/services.ts @@ -0,0 +1,14 @@ +// placeholder xstate async service that returns a set of theme cards + +export const fetchThemeCards = async () => { + return [ + { + name: 'Twenty Twenty One', + description: 'The default theme for WordPress.', + }, + { + name: 'Twenty Twenty', + description: 'The previous default theme for WordPress.', + }, + ]; +}; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx b/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx new file mode 100644 index 00000000000..a67b23534ae --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx @@ -0,0 +1,6 @@ +export type ThemeCard = { + // placeholder props, possibly take reference from https://github.com/Automattic/wp-calypso/blob/1f1b79210c49ef0d051f8966e24122229a334e29/packages/design-picker/src/components/theme-card/index.tsx#L32 + name: string; + description: string; + image: string; +}; diff --git a/plugins/woocommerce-admin/client/customize-store/types.ts b/plugins/woocommerce-admin/client/customize-store/types.ts new file mode 100644 index 00000000000..6d3da8f3ef6 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/types.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { Sender } from 'xstate'; + +/** + * Internal dependencies + */ +import { customizeStoreStateMachineEvents } from '.'; +import { ThemeCard } from './intro/theme-cards'; + +export type CustomizeStoreComponent = ( props: { + sendEvent: Sender< customizeStoreStateMachineEvents >; + context: customizeStoreStateMachineContext; +} ) => React.ReactElement | null; + +export type CustomizeStoreComponentMeta = { + component: CustomizeStoreComponent; +}; + +export type customizeStoreStateMachineContext = { + themeConfiguration: Record< string, unknown >; // placeholder for theme configuration until we know what it looks like + intro: { + themeCards: ThemeCard[]; + activeTheme: string; + }; +}; diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts index 3b167616353..00d154a182b 100644 --- a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts +++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts @@ -30,7 +30,7 @@ export const getTourConfig = ( { spotlight: { interactivity: { enabled: true, - rootElementSelector: '.woocommerce.wc-addons-wrap', + rootElementSelector: '.woocommerce-marketplace', }, }, autoScroll: { @@ -39,21 +39,6 @@ export const getTourConfig = ( { }, }, popperModifiers: [ - { - name: 'arrow', - options: { - padding: ( { - popper, - }: { - popper: { width: number }; - } ) => { - return { - // Align the arrow to the left of the popper. - right: popper.width - 34, - }; - }, - }, - }, { name: 'offset', options: { diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts index 5eb00fc2ba5..93fbc3e5a53 100644 --- a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts +++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts @@ -10,13 +10,15 @@ export const getSteps = (): TourKitTypes.WooStep[] => { return [ { referenceElements: { - desktop: '#adminmenu a[href="admin.php?page=wc-addons"]', + desktop: + '#adminmenu a[href="admin.php?page=wc-admin&path=%2Fextensions"]', }, focusElement: { - desktop: '#adminmenu a[href="admin.php?page=wc-addons"]', + desktop: + '#adminmenu a[href="admin.php?page=wc-admin&path=%2Fextensions"]', }, meta: { - name: 'wc-addons-menu-item', + name: 'wc-extensions-menu-item', heading: __( 'Welcome to the WooCommerce Marketplace', 'woocommerce' @@ -24,7 +26,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => { descriptions: { desktop: createInterpolateElement( __( - 'Power up your store by adding extra functionality using extensions, find a fresh new look with themes, or integrate your store with other software and services.

    The WooCommerce Marketplace is your go-to for all of the above, and the only place you’ll find products that have been reviewed and approved by the WooCommerce team.

    Whether you’re looking to improve your store or grow your business, you can find a solution here. There are hundreds of options available, and new products are added regularly.

    The WooCommerce Marketplace is also available at WooCommerce.com.', + "Power up your store by adding extra functionality with extensions or integrate your store with other software and services.

    Here you'll find hundreds of trusted solutions for your store — all reviewed and approved by the Woo team.

    You can also browse the Woo Marketplace at WooCommerce.com.", 'woocommerce' ), { @@ -36,17 +38,17 @@ export const getSteps = (): TourKitTypes.WooStep[] => { }, { referenceElements: { - desktop: '.marketplace-header__search-form', + desktop: '.woocommerce-marketplace__search', }, focusElement: { - desktop: '.marketplace-header__search-form', + desktop: '.woocommerce-marketplace__search', }, meta: { - name: 'wc-addons-search', + name: 'wc-extensions-search', heading: __( 'Find exactly what you need', 'woocommerce' ), descriptions: { desktop: __( - 'Use the search box to find specific products or solutions.', + 'Use the search box to find specific extensions or solutions.', 'woocommerce' ), }, @@ -54,10 +56,10 @@ export const getSteps = (): TourKitTypes.WooStep[] => { }, { referenceElements: { - desktop: '#marketplace-current-section-dropdown', + desktop: '.woocommerce-marketplace__tab-browse', }, focusElement: { - desktop: '#marketplace-current-section-dropdown', + desktop: '.woocommerce-marketplace__tab-browse', }, meta: { name: 'wc-addons-categories', @@ -65,7 +67,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => { descriptions: { desktop: createInterpolateElement( __( - 'Or browse all available products by category.', + "Or if you're not sure exactly what you need, you can browse all available extensions by category.", 'woocommerce' ), { @@ -77,18 +79,18 @@ export const getSteps = (): TourKitTypes.WooStep[] => { }, { referenceElements: { - desktop: '.addon-product-group:first-child', + desktop: '.woocommerce-marketplace__discover:first-child', }, focusElement: { - desktop: '.addon-product-group:first-child', + desktop: '.woocommerce-marketplace__discover:first-child', }, meta: { name: 'wc-addons-featured', - heading: __( 'Learn more about products', 'woocommerce' ), + heading: __( 'Learn more about each product', 'woocommerce' ), descriptions: { desktop: createInterpolateElement( __( - 'Scroll down to see all available products for a search or selected category.

    Click on any product to see more information about it, including features, requirements, and available documentation.', + 'Scroll down to see all of the relevant extensions and solutions.

    Click on any solution to learn more about its features, any installation requirements, and available documentation.', 'woocommerce' ), { @@ -100,10 +102,10 @@ export const getSteps = (): TourKitTypes.WooStep[] => { }, { referenceElements: { - desktop: '.marketplace-header__tab-link_helper', + desktop: '.woocommerce-marketplace__header-meta', }, focusElement: { - desktop: '.marketplace-header__tab-link_helper', + desktop: '.woocommerce-marketplace__header-meta', }, meta: { name: 'wc-addons-my-subscriptions', @@ -111,7 +113,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => { descriptions: { desktop: createInterpolateElement( __( - "Products purchased from the WooCommerce Marketplace can be managed in My Subscriptions, either here or on WooCommerce.com.

    Every purchase is backed by our 30-day money-back guarantee, and includes email and live chat support.

    That's it! We hope the WooCommerce Marketplace helps you build the business of your dreams.", + "All of your Woo Marketplace purchases can be found here, or on WooCommerce.com.

    Every purchase is backed by our 30-day money-back guarantee, and includes email and live chat support.

    That's it! You're now ready to power up your store.", 'woocommerce' ), { diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx index a8e310c7566..45de2d42fa9 100644 --- a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx +++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx @@ -16,7 +16,7 @@ import { scrollPopperToVisibleAreaIfNeeded } from './utils'; import { getSteps } from './get-steps'; const WCAddonsTour = () => { - const [ showTour, setShowTour ] = useState( false ); + const [ showTour, setShowTour ] = useState( true ); const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); @@ -62,7 +62,7 @@ const WCAddonsTour = () => { const timeoutId = setTimeout( showPopper, 500 ); const intervalId = observePositionChange( - '.wc-addons-wrap', + '.woocommerce-marketplace', showPopper, 150 ); diff --git a/plugins/woocommerce-admin/client/layout/controller.js b/plugins/woocommerce-admin/client/layout/controller.js index dc02f7f1f90..45c08375ced 100644 --- a/plugins/woocommerce-admin/client/layout/controller.js +++ b/plugins/woocommerce-admin/client/layout/controller.js @@ -57,6 +57,9 @@ const MarketingOverviewMultichannel = lazy( () => /* webpackChunkName: "multichannel-marketing" */ '../marketing/overview-multichannel' ) ); +const Marketplace = lazy( () => + import( /* webpackChunkName: "marketplace" */ '../marketplace' ) +); const ProfileWizard = lazy( () => import( /* webpackChunkName: "profile-wizard" */ '../profile-wizard' ) ); @@ -74,6 +77,10 @@ const WCPaymentsWelcomePage = lazy( () => ) ); +const CustomizeStore = lazy( () => + import( /* webpackChunkName: "customize-store" */ '../customize-store' ) +); + export const PAGES_FILTER = 'woocommerce_admin_pages_list'; export const getPages = () => { @@ -173,6 +180,25 @@ export const getPages = () => { } ); } + if ( isFeatureEnabled( 'marketplace' ) ) { + pages.push( { + container: Marketplace, + layout: { + header: false, + }, + path: '/extensions', + breadcrumbs: [ + [ '/extensions', __( 'Extensions', 'woocommerce' ) ], + __( 'Extensions', 'woocommerce' ), + ], + wpOpenMenu: 'toplevel_page_woocommerce', + capability: 'manage_woocommerce', + navArgs: { + id: 'woocommerce-marketplace', + }, + } ); + } + if ( isFeatureEnabled( 'product_block_editor' ) ) { const productPage = { container: ProductPage, @@ -290,6 +316,18 @@ export const getPages = () => { } ); } + if ( window.wcAdminFeatures[ 'customize-store' ] ) { + pages.push( { + container: CustomizeStore, + path: '/customize-store', + breadcrumbs: [ + ...initialBreadcrumbs, + __( 'Customize Your Store', 'woocommerce' ), + ], + capability: 'manage_woocommerce', + } ); + } + if ( window.wcAdminFeatures.settings ) { pages.push( { container: SettingsGroup, diff --git a/plugins/woocommerce-admin/client/marketplace/assets/images/no-results.svg b/plugins/woocommerce-admin/client/marketplace/assets/images/no-results.svg new file mode 100644 index 00000000000..676d0acaa7a --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/assets/images/no-results.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/woocommerce-admin/client/marketplace/assets/images/woo-icon.svg b/plugins/woocommerce-admin/client/marketplace/assets/images/woo-icon.svg new file mode 100644 index 00000000000..8ee74b674a1 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/assets/images/woo-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx new file mode 100644 index 00000000000..b4f8065bbc9 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-dropdown.tsx @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import { Dropdown } from '@wordpress/components'; +import { chevronDown, chevronUp, Icon } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { navigateTo, getNewPath } from '@woocommerce/navigation'; +import classNames from 'classnames'; + +/** + * Internal dependencies + */ +import { Category } from './types'; + +function DropdownContent( props: { + readonly categories: Category[]; + readonly selected?: Category; + readonly onClick: () => void; +} ): JSX.Element { + function updateCategorySelection( + event: React.MouseEvent< HTMLButtonElement > + ) { + const slug = event.currentTarget.value; + + if ( ! slug ) { + return; + } + + /** + * Trigger the onClick event on the parent component to close the dropdown. + * This closes the dropdown automatically when a user clicks on an item. + */ + props.onClick(); + + navigateTo( { + url: getNewPath( { category: slug } ), + } ); + } + + return ( +
      + { props.categories.map( ( category ) => ( +
    • + +
    • + ) ) } +
    + ); +} + +type CategoryDropdownProps = { + label: string; + categories: Category[]; + className?: string; + buttonClassName?: string; + contentClassName?: string; + arrowIconSize?: number; + selected?: Category; +}; + +export default function CategoryDropdown( + props: CategoryDropdownProps +): JSX.Element { + return ( + ( + + ) } + className={ props.className } + renderContent={ ( { onToggle } ) => ( + + ) } + contentClassName={ props.contentClassName } + /> + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx new file mode 100644 index 00000000000..a3efb6cde61 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-link.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; +import { navigateTo, getNewPath } from '@woocommerce/navigation'; + +/** + * Internal dependencies + */ +import { Category } from './types'; + +export default function CategoryLink( props: Category ): JSX.Element { + function updateCategorySelection( + event: React.MouseEvent< HTMLButtonElement > + ) { + const slug = event.currentTarget.value; + + if ( ! slug ) { + return; + } + + navigateTo( { + url: getNewPath( { category: slug } ), + } ); + } + + const classes = classNames( + 'woocommerce-marketplace__category-item-button', + { + 'woocommerce-marketplace__category-item-button--selected': + props.selected, + } + ); + + return ( + + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss new file mode 100644 index 00000000000..83b58a66884 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss @@ -0,0 +1,126 @@ +@import '../../stylesheets/_variables.scss'; + +.woocommerce-marketplace__category-selector { + display: flex; + align-items: stretch; + margin: $grid-unit-20 0 0 0; +} + +.woocommerce-marketplace__category-item { + cursor: pointer; + + .components-dropdown { + height: 100%; + } +} + +.woocommerce-marketplace__category-item-button { + display: flex; + align-items: center; + cursor: pointer; + border: none; + border-radius: 2px; + color: $wp-gray-60; + background-color: $wp-gray-0; + padding: 6px $grid-unit-10; + margin-right: $grid-unit-10; + line-height: 20px; + height: 100%; + + &--selected { + color: $white; + background-color: $gray-900; + fill: $white; + } +} + +.woocommerce-marketplace__category-item-content { + .components-popover__content { + min-width: 200px; + } +} + +.woocommerce-marketplace__category-selector--full-width { + display: none; + margin-top: $grid-unit-15; +} + +@media screen and (max-width: $break-medium) { + .woocommerce-marketplace__category-selector--full-width { + display: flex; + } + + .woocommerce-marketplace__category-selector { + display: none; + } +} + +.woocommerce-marketplace__category-dropdown { + width: 100%; +} + +.woocommerce-marketplace__category-dropdown-button { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + border: 1px solid $gray-600; + border-radius: 2px; + background-color: $white; + width: 100%; + font-size: 13px; + line-height: 20px; + padding: $grid-unit-15 $grid-unit-10; + text-align: left; +} + +.woocommerce-marketplace__category-dropdown-content { + background-color: $white; + color: $gray-900; + font-size: 13px; + min-width: 280px; + width: calc(100% - 32px); + + .components-popover__content { + width: 100%; + } +} + +.woocommerce-marketplace__category-dropdown-list { + margin: 0; + line-height: 20px; +} + +.woocommerce-marketplace__category-dropdown-item { + border-radius: 2px; + + &:hover { + background-color: $gutenberg-gray-100; + } +} + +.woocommerce-marketplace__category-dropdown-item-button { + border: none; + cursor: pointer; + background-color: inherit; + color: $gray-900; + text-align: left; + padding: 6px $grid-unit-10; + line-height: 20px; + width: 100%; + + &--selected { + color: $white; + background-color: $gray-900; + } +} + +.woocommerce-marketplace__category-selector-loading { + display: flex; + margin-top: $grid-unit-20; + + p { + margin: 0; + line-height: $grid-unit-30; + } +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx new file mode 100644 index 00000000000..ecff77d79c4 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { Spinner } from '@wordpress/components'; +import { useQuery } from '@woocommerce/navigation'; +import classNames from 'classnames'; + +/** + * Internal dependencies + */ +import CategoryLink from './category-link'; +import CategoryDropdown from './category-dropdown'; +import { Category, CategoryAPIItem } from './types'; +import { fetchCategories } from '../../utils/functions'; +import './category-selector.scss'; + +const ALL_CATEGORIES_SLUG = '_all'; + +export default function CategorySelector(): JSX.Element { + const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] ); + const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] ); + const [ selected, setSelected ] = useState< Category >(); + const [ isLoading, setIsLoading ] = useState( false ); + + const query = useQuery(); + + useEffect( () => { + // If no category is selected, show All as selected + let categoryToSearch = ALL_CATEGORIES_SLUG; + + if ( query.category ) { + categoryToSearch = query.category; + } + + const allCategories = visibleItems.concat( dropdownItems ); + + const selectedCategory = allCategories.find( + ( category ) => category.slug === categoryToSearch + ); + + if ( selectedCategory ) { + setSelected( selectedCategory ); + } + }, [ query, visibleItems, dropdownItems ] ); + + useEffect( () => { + setIsLoading( true ); + + fetchCategories() + .then( ( categoriesFromAPI: CategoryAPIItem[] ) => { + const categories: Category[] = categoriesFromAPI + .map( ( categoryAPIItem: CategoryAPIItem ): Category => { + return { + ...categoryAPIItem, + selected: false, + }; + } ) + .filter( ( category: Category ): boolean => { + // The "featured" category is returned from the API for legacy reasons, but we don't need it: + return category.slug !== '_featured'; + } ); + + // Split array into two from 7th item + const visibleCategoryItems = categories.slice( 0, 7 ); + const dropdownCategoryItems = categories.slice( 7 ); + + setVisibleItems( visibleCategoryItems ); + setDropdownItems( dropdownCategoryItems ); + } ) + .catch( () => { + setVisibleItems( [] ); + setDropdownItems( [] ); + } ) + .finally( () => { + setIsLoading( false ); + } ); + }, [] ); + + function mobileCategoryDropdownLabel() { + const allCategoriesText = __( 'All Categories', 'woocommerce' ); + + if ( ! selected ) { + return allCategoriesText; + } + + if ( selected.label === 'All' ) { + return allCategoriesText; + } + + return selected.label; + } + + function isSelectedInDropdown() { + if ( ! selected ) { + return false; + } + + return dropdownItems.find( + ( category ) => category.slug === selected.slug + ); + } + + if ( isLoading ) { + return ( +
    +

    { __( 'Loading categories…', 'woocommerce' ) }

    + +
    + ); + } + + return ( + <> +
      + { visibleItems.map( ( category ) => ( +
    • + +
    • + ) ) } +
    • + { dropdownItems.length > 0 && ( + + ) } +
    • +
    + +
    + +
    + + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/types.ts b/plugins/woocommerce-admin/client/marketplace/components/category-selector/types.ts new file mode 100644 index 00000000000..9fd34be90d8 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/types.ts @@ -0,0 +1,10 @@ +export type Category = { + readonly slug: string; + readonly label: string; + selected: boolean; +}; + +export type CategoryAPIItem = { + readonly slug: string; + readonly label: string; +}; diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts new file mode 100644 index 00000000000..548249734e2 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts @@ -0,0 +1,3 @@ +export const DEFAULT_TAB_KEY = 'discover'; +export const MARKETPLACE_PATH = '/extensions'; +export const MARKETPLACE_URL = 'https://woocommerce.com'; diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.scss b/plugins/woocommerce-admin/client/marketplace/components/content/content.scss new file mode 100644 index 00000000000..c9456d07c4a --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.scss @@ -0,0 +1,14 @@ +@import '../../stylesheets/_variables.scss'; + +.woocommerce-marketplace__content { + box-sizing: content-box; + margin: auto; + max-width: $content-max-width; + padding: $header-height-mobile $content-spacing-small $content-spacing-small; +} + +@media screen and (min-width: $breakpoint-medium) { + .woocommerce-marketplace__content { + padding: $header-height-desktop $content-spacing-large $content-spacing-large; + } +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx new file mode 100644 index 00000000000..9c9af5fb837 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx @@ -0,0 +1,38 @@ +/** + * External dependencies + */ + +/** + * Internal dependencies + */ +import './content.scss'; +import Discover from '../discover/discover'; +import Extensions from '../extensions/extensions'; +import Footer from '../footer/footer'; +import FeedbackModal from '../feedback-modal/feedback-modal'; + +export interface ContentProps { + selectedTab?: string | undefined; +} + +const renderContent = ( selectedTab?: string ): JSX.Element => { + switch ( selectedTab ) { + case 'extensions': + return ; + default: + return ; + } +}; + +export default function Content( props: ContentProps ): JSX.Element { + const { selectedTab } = props; + return ( + <> +
    + { renderContent( selectedTab ) } +
    +