Merge branch 'add/49335-related-products-collection' into add/50860-upsells
This commit is contained in:
commit
d4379bae99
|
@ -4,23 +4,25 @@ on:
|
||||||
- cron: '0 0 * * *' # Run at 12 AM UTC.
|
- cron: '0 0 * * *' # Run at 12 AM UTC.
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: {}
|
env:
|
||||||
|
SOURCE_REF: trunk
|
||||||
|
TARGET_REF: nightly
|
||||||
|
RELEASE_ID: 25945111
|
||||||
|
|
||||||
|
permissions: { }
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: github.repository_owner == 'woocommerce'
|
if: github.repository_owner == 'woocommerce'
|
||||||
name: Nightly builds
|
name: Nightly builds
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
build: [trunk]
|
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.build }}
|
ref: ${{ env.SOURCE_REF }}
|
||||||
|
|
||||||
- name: Setup WooCommerce Monorepo
|
- name: Setup WooCommerce Monorepo
|
||||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||||
|
@ -31,26 +33,31 @@ jobs:
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
run: bash bin/build-zip.sh
|
run: bash bin/build-zip.sh
|
||||||
|
|
||||||
- name: Deploy nightly build
|
- name: Upload nightly build
|
||||||
uses: WebFreak001/deploy-nightly@v1.1.0
|
uses: WebFreak001/deploy-nightly@46ecbabd7fad70d3e7d2c97fe8cd54e7a52e215b #v3.2.0
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/25945111/assets{?name,label}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
release_id: 25945111
|
upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/${{ env.RELEASE_ID }}/assets{?name,label}
|
||||||
|
release_id: ${{ env.RELEASE_ID }}
|
||||||
asset_path: plugins/woocommerce/woocommerce.zip
|
asset_path: plugins/woocommerce/woocommerce.zip
|
||||||
asset_name: woocommerce-${{ matrix.build }}-nightly.zip
|
asset_name: woocommerce-${{ env.SOURCE_REF }}-nightly.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
max_releases: 1
|
max_releases: 1
|
||||||
update:
|
|
||||||
name: Update nightly tag commit ref
|
- name: Update nightly tag commit ref
|
||||||
runs-on: ubuntu-20.04
|
uses: actions/github-script@v7
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Update nightly tag
|
|
||||||
uses: richardsimko/github-tag-action@v1.0.5
|
|
||||||
with:
|
with:
|
||||||
tag_name: nightly
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
env:
|
script: |
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
const sourceRef = process.env.SOURCE_REF;
|
||||||
|
const targetRef = process.env.TARGET_REF;
|
||||||
|
const branchData = await github.rest.repos.getBranch({
|
||||||
|
...context.repo,
|
||||||
|
branch: sourceRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.git.updateRef({
|
||||||
|
...context.repo,
|
||||||
|
ref: `tags/${ targetRef }`,
|
||||||
|
sha: branchData.data.commit.sha,
|
||||||
|
});
|
||||||
|
|
15
CODEOWNERS
15
CODEOWNERS
|
@ -1 +1,14 @@
|
||||||
/.github/ @woocommerce/atlas
|
# Monorepo CI and package managers manifests.
|
||||||
|
/.github/ @woocommerce/flux
|
||||||
|
**/composer.json @woocommerce/flux
|
||||||
|
**/package.json @woocommerce/flux
|
||||||
|
|
||||||
|
# Monorepo tooling folders.
|
||||||
|
/bin/ @woocommerce/flux
|
||||||
|
/tools/ @woocommerce/flux
|
||||||
|
/packages/js/eslint-plugin/ @woocommerce/flux
|
||||||
|
/packages/js/dependency-extraction-webpack-plugin/ @woocommerce/flux
|
||||||
|
|
||||||
|
# Files in root of repository
|
||||||
|
/.* @woocommerce/flux
|
||||||
|
/*.* @woocommerce/flux
|
||||||
|
|
|
@ -20,8 +20,8 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
|
||||||
Once you've installed all of the prerequisites, you can run the following commands to get everything working.
|
Once you've installed all of the prerequisites, you can run the following commands to get everything working.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ensure that you're using the correct version of Node
|
# Ensure that correct version of Node is installed and being used
|
||||||
nvm use
|
nvm install
|
||||||
# Install the PHP and Composer dependencies for all of the plugins, packages, and tools
|
# Install the PHP and Composer dependencies for all of the plugins, packages, and tools
|
||||||
pnpm install
|
pnpm install
|
||||||
# Build all of the plugins, packages, and tools in the monorepo
|
# Build all of the plugins, packages, and tools in the monorepo
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 9.3.1 2024-09-12 =
|
||||||
|
|
||||||
|
* Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312)
|
||||||
|
|
||||||
= 9.3.0 2024-09-10 =
|
= 9.3.0 2024-09-10 =
|
||||||
|
|
||||||
**WooCommerce**
|
**WooCommerce**
|
||||||
|
@ -141,6 +145,7 @@
|
||||||
* Update - Update core profiler continue button container on extension screen [#50582](https://github.com/woocommerce/woocommerce/pull/50582)
|
* Update - Update core profiler continue button container on extension screen [#50582](https://github.com/woocommerce/woocommerce/pull/50582)
|
||||||
* Update - Update Store Alert actions to have unique keys. [#50424](https://github.com/woocommerce/woocommerce/pull/50424)
|
* Update - Update Store Alert actions to have unique keys. [#50424](https://github.com/woocommerce/woocommerce/pull/50424)
|
||||||
* Update - Update WooCommercePayments task is_supported to use default suggestions [#50585](https://github.com/woocommerce/woocommerce/pull/50585)
|
* Update - Update WooCommercePayments task is_supported to use default suggestions [#50585](https://github.com/woocommerce/woocommerce/pull/50585)
|
||||||
|
* Update - Enhance CSV path and upload handling in product import [#51344](https://github.com/woocommerce/woocommerce/pull/51344)
|
||||||
* Dev - Execute test env setup on host instead of wp-env container [#51021](https://github.com/woocommerce/woocommerce/pull/51021)
|
* Dev - Execute test env setup on host instead of wp-env container [#51021](https://github.com/woocommerce/woocommerce/pull/51021)
|
||||||
* Dev - Added code docs with examples to the Analytics classes [#49425](https://github.com/woocommerce/woocommerce/pull/49425)
|
* Dev - Added code docs with examples to the Analytics classes [#49425](https://github.com/woocommerce/woocommerce/pull/49425)
|
||||||
* Dev - Add lost password e2e tests [#50611](https://github.com/woocommerce/woocommerce/pull/50611)
|
* Dev - Add lost password e2e tests [#50611](https://github.com/woocommerce/woocommerce/pull/50611)
|
||||||
|
|
|
@ -727,7 +727,7 @@
|
||||||
"menu_title": "Implement merchant onboarding",
|
"menu_title": "Implement merchant onboarding",
|
||||||
"tags": "how-to",
|
"tags": "how-to",
|
||||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-merchant-onboarding.md",
|
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-merchant-onboarding.md",
|
||||||
"hash": "c73e3c5015e6cda3be9ebd2d5fbda590ac9fa599e5fb02163c971c01060970ad",
|
"hash": "85fc7d70f47fdb195ad2c690d3b95565169221f9e4d7afa98e88f3824ad0e267",
|
||||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-merchant-onboarding.md",
|
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-merchant-onboarding.md",
|
||||||
"id": "89fe15dc232379f546852822230c334d3d940b93"
|
"id": "89fe15dc232379f546852822230c334d3d940b93"
|
||||||
},
|
},
|
||||||
|
@ -874,7 +874,7 @@
|
||||||
"menu_title": "Development environment setup",
|
"menu_title": "Development environment setup",
|
||||||
"tags": "tutorial, setup",
|
"tags": "tutorial, setup",
|
||||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/development-environment.md",
|
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/development-environment.md",
|
||||||
"hash": "9e471d3f44a882fe61dcad9e5207d51b280a7220aae1bf6e4ae1fbdd68b7e3d4",
|
"hash": "bf5d77349ea64d1b8e19fe6b7472be35ed92406c5aafe677ce92363fb13f94d4",
|
||||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/development-environment.md",
|
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/development-environment.md",
|
||||||
"id": "9080572a3904349c44c565ca7e1bef1212c58757"
|
"id": "9080572a3904349c44c565ca7e1bef1212c58757"
|
||||||
},
|
},
|
||||||
|
@ -1804,5 +1804,5 @@
|
||||||
"categories": []
|
"categories": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hash": "1f651a59399c34644d2f91a0366bbd01da2c7dc677a1c53329b184badd3b8d13"
|
"hash": "dfe48a2a48d383c1f4e5b5bb4258d369dd4e80ac8582462b16bbfedd879cb0bf"
|
||||||
}
|
}
|
|
@ -448,7 +448,7 @@ class ExampleNote {
|
||||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||||
|
|
||||||
// Set the type of layout the note uses. Supported layout types are:
|
// Set the type of layout the note uses. Supported layout types are:
|
||||||
// 'banner', 'plain', 'thumbnail'
|
// 'plain', 'thumbnail'
|
||||||
$note->set_layout( 'plain' );
|
$note->set_layout( 'plain' );
|
||||||
|
|
||||||
// Set the image for the note. This property renders as the src
|
// Set the image for the note. This property renders as the src
|
||||||
|
|
|
@ -80,22 +80,16 @@ git clone https://github.com/woocommerce/woocommerce.git
|
||||||
cd woocommerce
|
cd woocommerce
|
||||||
```
|
```
|
||||||
|
|
||||||
### Activate the required Node version
|
### Install and Activate the required Node version
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nvm use
|
nvm install
|
||||||
Found '/path/to/woocommerce/.nvmrc' with version <v12>
|
Found '/path/to/woocommerce/.nvmrc' with version <v20>
|
||||||
Now using node v12.21.0 (npm v6.14.11)
|
v20.17.0 is already installed.
|
||||||
|
Now using node v20.17.0 (npm v10.8.2)
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: if you don't have the required version of Node installed, NVM will alert you so you can install it:
|
Note: if you don't have the required version of Node installed, NVM will install it.
|
||||||
|
|
||||||
```sh
|
|
||||||
Found '/path/to/woocommerce/.nvmrc' with version <v12>
|
|
||||||
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
|
### Install dependencies
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Use page query param consistently as string for `getReportTableQuery`.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Removed the leftover user meta from the help panel spotlight
|
|
@ -561,7 +561,7 @@ export function getReportTableQuery(
|
||||||
before: noIntervals
|
before: noIntervals
|
||||||
? undefined
|
? undefined
|
||||||
: appendTimestamp( datesFromQuery.primary.before, 'end' ),
|
: appendTimestamp( datesFromQuery.primary.before, 'end' ),
|
||||||
page: query.paged || 1,
|
page: query.paged || '1',
|
||||||
per_page: query.per_page || QUERY_DEFAULTS.pageSize,
|
per_page: query.per_page || QUERY_DEFAULTS.pageSize,
|
||||||
...filterQuery,
|
...filterQuery,
|
||||||
...tableQuery,
|
...tableQuery,
|
||||||
|
|
|
@ -15,7 +15,6 @@ export type UserPreferences = {
|
||||||
dashboard_chart_type?: string;
|
dashboard_chart_type?: string;
|
||||||
dashboard_leaderboard_rows?: string;
|
dashboard_leaderboard_rows?: string;
|
||||||
dashboard_sections?: string;
|
dashboard_sections?: string;
|
||||||
help_panel_highlight_shown?: string;
|
|
||||||
homepage_layout?: string;
|
homepage_layout?: string;
|
||||||
homepage_stats?: string;
|
homepage_stats?: string;
|
||||||
orders_report_columns?: string;
|
orders_report_columns?: string;
|
||||||
|
|
|
@ -300,15 +300,28 @@ const exitToWooHome = fromPromise( async () => {
|
||||||
window.location.href = getNewPath( {}, '/', {} );
|
window.location.href = getNewPath( {}, '/', {} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
const getPluginNameParam = (
|
||||||
|
pluginsSelected: CoreProfilerStateMachineContext[ 'pluginsSelected' ]
|
||||||
|
) => {
|
||||||
|
if ( pluginsSelected.includes( 'woocommerce-payments' ) ) {
|
||||||
|
return 'woocommerce-payments';
|
||||||
|
}
|
||||||
|
return 'jetpack-ai';
|
||||||
|
};
|
||||||
|
|
||||||
const redirectToJetpackAuthPage = ( {
|
const redirectToJetpackAuthPage = ( {
|
||||||
event,
|
event,
|
||||||
|
context,
|
||||||
}: {
|
}: {
|
||||||
context: CoreProfilerStateMachineContext;
|
context: CoreProfilerStateMachineContext;
|
||||||
event: { output: { url: string } };
|
event: { output: { url: string } };
|
||||||
} ) => {
|
} ) => {
|
||||||
const url = new URL( event.output.url );
|
const url = new URL( event.output.url );
|
||||||
url.searchParams.set( 'installed_ext_success', '1' );
|
url.searchParams.set( 'installed_ext_success', '1' );
|
||||||
url.searchParams.set( 'plugin_name', 'jetpack-ai' );
|
url.searchParams.set(
|
||||||
|
'plugin_name',
|
||||||
|
getPluginNameParam( context.pluginsSelected )
|
||||||
|
);
|
||||||
window.location.href = url.toString();
|
window.location.href = url.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@ import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/compo
|
||||||
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
||||||
import { isEqual, noop } from 'lodash';
|
import { isEqual, noop } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { trackEvent } from '~/customize-store/tracking';
|
||||||
|
|
||||||
const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
|
const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
|
||||||
|
|
||||||
// Removes the typography settings from the styles when the user is changing
|
// Removes the typography settings from the styles when the user is changing
|
||||||
|
@ -100,6 +105,24 @@ export const VariationContainer = ( { variation, children } ) => {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
if ( variation.settings.color?.palette ) {
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_color_palette_item_click',
|
||||||
|
{
|
||||||
|
item: variation.title,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( variation.settings.typography ) {
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_typography_item_click',
|
||||||
|
{
|
||||||
|
item: variation.title,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectOnEnter = ( event ) => {
|
const selectOnEnter = ( event ) => {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
||||||
import { CustomizeStoreContext } from '../';
|
import { CustomizeStoreContext } from '../';
|
||||||
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
|
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
|
||||||
import { ColorPalette, ColorPanel } from './global-styles';
|
import { ColorPalette, ColorPanel } from './global-styles';
|
||||||
|
import { trackEvent } from '~/customize-store/tracking';
|
||||||
import { FlowType } from '~/customize-store/types';
|
import { FlowType } from '~/customize-store/types';
|
||||||
|
|
||||||
const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
|
const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
|
||||||
|
@ -27,6 +28,14 @@ const SidebarNavigationScreenColorPaletteContent = () => {
|
||||||
const hasCreatedOwnColors = !! (
|
const hasCreatedOwnColors = !! (
|
||||||
user.settings.color && user.settings.color.palette.hasCreatedOwnColors
|
user.settings.color && user.settings.color.palette.hasCreatedOwnColors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handlePanelBodyToggle( open?: boolean ) {
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_color_palette_create_toggle',
|
||||||
|
{ open }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are
|
// Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are
|
||||||
// loaded. This is necessary because the Iframe component waits until
|
// loaded. This is necessary because the Iframe component waits until
|
||||||
// the block editor store's `__internalIsInitialized` is true before
|
// the block editor store's `__internalIsInitialized` is true before
|
||||||
|
@ -45,6 +54,7 @@ const SidebarNavigationScreenColorPaletteContent = () => {
|
||||||
className="woocommerce-customize-store__color-panel-container"
|
className="woocommerce-customize-store__color-panel-container"
|
||||||
title={ __( 'or create your own', 'woocommerce' ) }
|
title={ __( 'or create your own', 'woocommerce' ) }
|
||||||
initialOpen={ hasCreatedOwnColors }
|
initialOpen={ hasCreatedOwnColors }
|
||||||
|
onToggle={ handlePanelBodyToggle }
|
||||||
>
|
>
|
||||||
<ColorPanel />
|
<ColorPanel />
|
||||||
</PanelBody>
|
</PanelBody>
|
||||||
|
|
|
@ -297,16 +297,26 @@ const LogoEdit = ( {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMediaUploadSelect( media: { id: string; url: string } ) {
|
||||||
|
onInitialSelectLogo( media );
|
||||||
|
trackEvent( 'customize_your_store_assembler_hub_logo_select' );
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! logoUrl ) {
|
if ( ! logoUrl ) {
|
||||||
return (
|
return (
|
||||||
<MediaUploadCheck>
|
<MediaUploadCheck>
|
||||||
<MediaUpload
|
<MediaUpload
|
||||||
onSelect={ onInitialSelectLogo }
|
onSelect={ handleMediaUploadSelect }
|
||||||
allowedTypes={ ALLOWED_MEDIA_TYPES }
|
allowedTypes={ ALLOWED_MEDIA_TYPES }
|
||||||
render={ ( { open }: { open: () => void } ) => (
|
render={ ( { open }: { open: () => void } ) => (
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={ open }
|
onClick={ () => {
|
||||||
|
open();
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_logo_add_click'
|
||||||
|
);
|
||||||
|
} }
|
||||||
className="block-library-site-logo__inspector-upload-container"
|
className="block-library-site-logo__inspector-upload-container"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
@ -351,10 +361,17 @@ const LogoEdit = ( {
|
||||||
<>
|
<>
|
||||||
<MediaUploadCheck>
|
<MediaUploadCheck>
|
||||||
<MediaUpload
|
<MediaUpload
|
||||||
onSelect={ onInitialSelectLogo }
|
onSelect={ handleMediaUploadSelect }
|
||||||
allowedTypes={ ALLOWED_MEDIA_TYPES }
|
allowedTypes={ ALLOWED_MEDIA_TYPES }
|
||||||
render={ ( { open }: { open: () => void } ) =>
|
render={ ( { open }: { open: () => void } ) =>
|
||||||
cloneElement( logoImg, { onClick: open } )
|
cloneElement( logoImg, {
|
||||||
|
onClick() {
|
||||||
|
open();
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_logo_edit_click'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} )
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</MediaUploadCheck>
|
</MediaUploadCheck>
|
||||||
|
@ -478,6 +495,9 @@ export const SidebarNavigationScreenLogo = ( {
|
||||||
media
|
media
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_logo_select'
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
allowedTypes={
|
allowedTypes={
|
||||||
ALLOWED_MEDIA_TYPES
|
ALLOWED_MEDIA_TYPES
|
||||||
|
@ -490,6 +510,9 @@ export const SidebarNavigationScreenLogo = ( {
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
open();
|
open();
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_logo_replace_click'
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
{ __(
|
{ __(
|
||||||
|
@ -508,6 +531,9 @@ export const SidebarNavigationScreenLogo = ( {
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
onClose();
|
onClose();
|
||||||
onRemoveLogo();
|
onRemoveLogo();
|
||||||
|
trackEvent(
|
||||||
|
'customize_your_store_assembler_hub_logo_remove_click'
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
{ __(
|
{ __(
|
||||||
|
|
|
@ -8,8 +8,18 @@
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__content .woocommerce-customize-store-banner .woocommerce-customize-store-banner-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $breakpoint-medium) {
|
@media screen and (min-width: $breakpoint-medium) {
|
||||||
.woocommerce-marketplace__content {
|
.woocommerce-marketplace__content {
|
||||||
padding: $grid-unit-60 $grid-unit-40;
|
padding: $grid-unit-60 $grid-unit-40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $breakpoint-medium) {
|
||||||
|
.woocommerce-marketplace__content .woocommerce-customize-store-banner .woocommerce-customize-store-banner-content {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -73,7 +73,8 @@ export default function Content(): JSX.Element {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
query.tab === undefined ||
|
query.tab === undefined ||
|
||||||
( query.tab && [ '', 'discover' ].includes( query.tab ) )
|
( query.tab &&
|
||||||
|
[ '', 'discover', 'my-subscriptions' ].includes( query.tab ) )
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -196,6 +197,9 @@ export default function Content(): JSX.Element {
|
||||||
{ selectedTab !== 'business-services' && (
|
{ selectedTab !== 'business-services' && (
|
||||||
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
||||||
) }
|
) }
|
||||||
|
{ selectedTab !== 'business-services' && (
|
||||||
|
<SubscriptionsExpiredExpiringNotice type="missing" />
|
||||||
|
) }
|
||||||
|
|
||||||
{ renderContent() }
|
{ renderContent() }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ProductLoader from '../product-loader/product-loader';
|
||||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||||
import { ProductType } from '../product-list/types';
|
import { ProductType } from '../product-list/types';
|
||||||
import './discover.scss';
|
import './discover.scss';
|
||||||
|
import { recordMarketplaceView } from '~/marketplace/utils/tracking';
|
||||||
|
|
||||||
export default function Discover(): JSX.Element | null {
|
export default function Discover(): JSX.Element | null {
|
||||||
const [ productGroups, setProductGroups ] = useState<
|
const [ productGroups, setProductGroups ] = useState<
|
||||||
|
@ -28,10 +29,17 @@ export default function Discover(): JSX.Element | null {
|
||||||
return product.id;
|
return product.id;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// This is a new event specific to the Discover tab, added with Woo 8.4.
|
||||||
recordEvent( 'marketplace_discover_viewed', {
|
recordEvent( 'marketplace_discover_viewed', {
|
||||||
view: 'discover',
|
view: 'discover',
|
||||||
product_ids,
|
product_ids,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// This is the new page view event added with Woo 8.3. It's improved with the marketplace_discover_viewed event
|
||||||
|
// but we'll keep it for a while to keep it compatible.
|
||||||
|
recordMarketplaceView( {
|
||||||
|
view: 'discover',
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the content for this screen
|
// Get the content for this screen
|
||||||
|
|
|
@ -9,6 +9,9 @@
|
||||||
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
|
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
|
||||||
padding: 0 $content-spacing-large;
|
padding: 0 $content-spacing-large;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: sticky;
|
||||||
|
top: var(--wp-admin--admin-bar--height, 32px);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
/* On narrow screens, "stack" header items and hide the bottom border */
|
/* On narrow screens, "stack" header items and hide the bottom border */
|
||||||
@media (width <= $breakpoint-medium) {
|
@media (width <= $breakpoint-medium) {
|
||||||
|
|
|
@ -39,6 +39,12 @@ export default function SubscriptionsExpiredExpiringNotice(
|
||||||
dismissed:
|
dismissed:
|
||||||
'woo_subscription_expiring_notice_in_marketplace_dismissed',
|
'woo_subscription_expiring_notice_in_marketplace_dismissed',
|
||||||
},
|
},
|
||||||
|
'woo-subscription-missing-notice': {
|
||||||
|
shown: 'woo_subscription_missing_notice_in_marketplace_shown',
|
||||||
|
clicked: 'woo_subscription_missing_notice_in_marketplace_clicked',
|
||||||
|
dismissed:
|
||||||
|
'woo_subscription_missing_notice_in_marketplace_dismissed',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let notice = null;
|
let notice = null;
|
||||||
|
@ -51,6 +57,9 @@ export default function SubscriptionsExpiredExpiringNotice(
|
||||||
} else if ( type === 'expiring' ) {
|
} else if ( type === 'expiring' ) {
|
||||||
notice = wccomSettings?.subscription_expiring_notice || {};
|
notice = wccomSettings?.subscription_expiring_notice || {};
|
||||||
notice_id = 'woo-subscription-expiring-notice';
|
notice_id = 'woo-subscription-expiring-notice';
|
||||||
|
} else if ( type === 'missing' ) {
|
||||||
|
notice = wccomSettings?.subscription_missing_notice || {};
|
||||||
|
notice_id = 'woo-subscription-missing-notice';
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,48 @@ export default function Install( props: InstallProps ) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const handleInstallError = ( error: any ) => {
|
||||||
|
loadSubscriptions( false ).then( () => {
|
||||||
|
let errorMessage = sprintf(
|
||||||
|
// translators: %s is the product name.
|
||||||
|
__( '%s couldn’t be installed.', 'woocommerce' ),
|
||||||
|
props.subscription.product_name
|
||||||
|
);
|
||||||
|
if ( error?.success === false && error?.data.message ) {
|
||||||
|
errorMessage += ' ' + error.data.message;
|
||||||
|
}
|
||||||
|
addNotice(
|
||||||
|
props.subscription.product_key,
|
||||||
|
errorMessage,
|
||||||
|
NoticeStatus.Error,
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Download and install manually',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
url: 'https://woocommerce.com/my-account/downloads/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
stopInstall();
|
||||||
|
|
||||||
|
if ( props.onError ) {
|
||||||
|
props.onError();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
recordEvent( 'marketplace_product_install_failed', {
|
||||||
|
product_zip_slug: props.subscription.zip_slug,
|
||||||
|
product_id: props.subscription.product_id,
|
||||||
|
product_current_version: props.subscription.version,
|
||||||
|
error_message: error?.data?.message,
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
const install = () => {
|
const install = () => {
|
||||||
recordEvent( 'marketplace_product_install_button_clicked', {
|
recordEvent( 'marketplace_product_install_button_clicked', {
|
||||||
product_zip_slug: props.subscription.zip_slug,
|
product_zip_slug: props.subscription.zip_slug,
|
||||||
|
@ -90,71 +132,26 @@ export default function Install( props: InstallProps ) {
|
||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
}
|
}
|
||||||
} )
|
} )
|
||||||
.catch( ( error ) => {
|
.catch( handleInstallError );
|
||||||
loadSubscriptions( false ).then( () => {
|
} else {
|
||||||
let errorMessage = sprintf(
|
getInstallUrl( props.subscription )
|
||||||
// translators: %s is the product name.
|
.then( ( url: string ) => {
|
||||||
__( '%s couldn’t be installed.', 'woocommerce' ),
|
recordEvent( 'marketplace_product_install_url', {
|
||||||
props.subscription.product_name
|
|
||||||
);
|
|
||||||
if ( error?.success === false && error?.data.message ) {
|
|
||||||
errorMessage += ' ' + error.data.message;
|
|
||||||
}
|
|
||||||
addNotice(
|
|
||||||
props.subscription.product_key,
|
|
||||||
errorMessage,
|
|
||||||
NoticeStatus.Error,
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: __( 'Try again', 'woocommerce' ),
|
|
||||||
onClick: install,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
stopInstall();
|
|
||||||
|
|
||||||
if ( props.onError ) {
|
|
||||||
props.onError();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
recordEvent( 'marketplace_product_install_failed', {
|
|
||||||
product_zip_slug: props.subscription.zip_slug,
|
product_zip_slug: props.subscription.zip_slug,
|
||||||
product_id: props.subscription.product_id,
|
product_id: props.subscription.product_id,
|
||||||
product_current_version: props.subscription.version,
|
product_current_version: props.subscription.version,
|
||||||
error_message: error?.data?.message,
|
product_install_url: url,
|
||||||
} );
|
} );
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
getInstallUrl( props.subscription ).then( ( url: string ) => {
|
|
||||||
recordEvent( 'marketplace_product_install_url', {
|
|
||||||
product_zip_slug: props.subscription.zip_slug,
|
|
||||||
product_id: props.subscription.product_id,
|
|
||||||
product_current_version: props.subscription.version,
|
|
||||||
product_install_url: url,
|
|
||||||
} );
|
|
||||||
|
|
||||||
stopInstall();
|
stopInstall();
|
||||||
|
|
||||||
if ( url ) {
|
if ( url ) {
|
||||||
window.open( url, '_self' );
|
window.open( url, '_self' );
|
||||||
} else {
|
} else {
|
||||||
addNotice(
|
throw new Error();
|
||||||
props.subscription.product_key,
|
}
|
||||||
sprintf(
|
} )
|
||||||
// translators: %s is the product name.
|
.catch( handleInstallError );
|
||||||
__(
|
|
||||||
'%s couldn’t be installed. Please install the product manually.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
props.subscription.product_name
|
|
||||||
),
|
|
||||||
NoticeStatus.Error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wpbody-content {
|
|
||||||
/* Prevent double-scrollbar issue on WooCommerce > Extension pages */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-layout__primary {
|
.woocommerce-layout__primary {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import domReady from '@wordpress/dom-ready';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
|
domReady( () => {
|
||||||
|
const purchaseSubscriptionLink = document.querySelectorAll(
|
||||||
|
'.woocommerce-purchase-subscription'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( purchaseSubscriptionLink.length > 0 ) {
|
||||||
|
recordEvent( 'woo_purchase_subscription_in_plugins_shown' );
|
||||||
|
purchaseSubscriptionLink.forEach( ( link ) => {
|
||||||
|
link.addEventListener( 'click', function () {
|
||||||
|
recordEvent( 'woo_purchase_subscription_in_plugins_clicked' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
|
@ -83,7 +83,7 @@ class SimpleInboxNote {
|
||||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||||
|
|
||||||
// Set the type of layout the note uses. Supported layout types are:
|
// Set the type of layout the note uses. Supported layout types are:
|
||||||
// 'banner', 'plain', 'thumbnail'.
|
// 'plain', 'thumbnail'.
|
||||||
$note->set_layout( 'plain' );
|
$note->set_layout( 'plain' );
|
||||||
|
|
||||||
// Set the image for the note. This property renders as the src
|
// Set the image for the note. This property renders as the src
|
||||||
|
|
|
@ -73,7 +73,6 @@ function get_mock_note_data() {
|
||||||
return array(
|
return array(
|
||||||
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
|
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
|
||||||
'info' => array(
|
'info' => array(
|
||||||
'banner' => $plugin_url . 'images/admin-notes/banner.jpg',
|
|
||||||
'thumbnail' => $plugin_url . 'images/admin-notes/thumbnail.jpg',
|
'thumbnail' => $plugin_url . 'images/admin-notes/thumbnail.jpg',
|
||||||
'plain' => '',
|
'plain' => '',
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Deprecate unsupported Inbox note banner layout
|
|
@ -1,114 +1,26 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import {
|
import {
|
||||||
useInnerBlockLayoutContext,
|
useInnerBlockLayoutContext,
|
||||||
useProductDataContext,
|
useProductDataContext,
|
||||||
} from '@woocommerce/shared-context';
|
} from '@woocommerce/shared-context';
|
||||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||||
import { isNumber, ProductResponseItem } from '@woocommerce/types';
|
import {
|
||||||
|
ProductRating,
|
||||||
|
getAverageRating,
|
||||||
|
getRatingCount,
|
||||||
|
} from '@woocommerce/editor-components/product-rating';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
type RatingProps = {
|
|
||||||
reviews: number;
|
|
||||||
rating: number;
|
|
||||||
parentClassName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAverageRating = (
|
|
||||||
product: Omit< ProductResponseItem, 'average_rating' > & {
|
|
||||||
average_rating: string;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const rating = parseFloat( product.average_rating );
|
|
||||||
|
|
||||||
return Number.isFinite( rating ) && rating > 0 ? rating : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRatingCount = ( product: ProductResponseItem ) => {
|
|
||||||
const count = isNumber( product.review_count )
|
|
||||||
? product.review_count
|
|
||||||
: parseInt( product.review_count, 10 );
|
|
||||||
|
|
||||||
return Number.isFinite( count ) && count > 0 ? count : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStarStyle = ( rating: number ) => ( {
|
|
||||||
width: ( rating / 5 ) * 100 + '%',
|
|
||||||
} );
|
|
||||||
|
|
||||||
const NoRating = ( { parentClassName }: { parentClassName: string } ) => {
|
|
||||||
const starStyle = getStarStyle( 0 );
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={ clsx(
|
|
||||||
'wc-block-components-product-rating-stars__norating-container',
|
|
||||||
`${ parentClassName }-product-rating-stars__norating-container`
|
|
||||||
) }
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'wc-block-components-product-rating-stars__norating'
|
|
||||||
}
|
|
||||||
role="img"
|
|
||||||
>
|
|
||||||
<span style={ starStyle } />
|
|
||||||
</div>
|
|
||||||
<span>{ __( 'No Reviews', 'woocommerce' ) }</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Rating = ( props: RatingProps ): JSX.Element => {
|
|
||||||
const { rating, reviews, parentClassName } = props;
|
|
||||||
|
|
||||||
const starStyle = getStarStyle( rating );
|
|
||||||
|
|
||||||
const ratingText = sprintf(
|
|
||||||
/* translators: %f is referring to the average rating value */
|
|
||||||
__( 'Rated %f out of 5', 'woocommerce' ),
|
|
||||||
rating
|
|
||||||
);
|
|
||||||
|
|
||||||
const ratingHTML = {
|
|
||||||
__html: sprintf(
|
|
||||||
/* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */
|
|
||||||
_n(
|
|
||||||
'Rated %1$s out of 5 based on %2$s customer rating',
|
|
||||||
'Rated %1$s out of 5 based on %2$s customer ratings',
|
|
||||||
reviews,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
sprintf( '<strong class="rating">%f</strong>', rating ),
|
|
||||||
sprintf( '<span class="rating">%d</span>', reviews )
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={ clsx(
|
|
||||||
'wc-block-components-product-rating-stars__stars',
|
|
||||||
`${ parentClassName }__product-rating-stars__stars`
|
|
||||||
) }
|
|
||||||
role="img"
|
|
||||||
aria-label={ ratingText }
|
|
||||||
>
|
|
||||||
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ProductRatingStarsProps {
|
interface ProductRatingStarsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
textAlign?: string;
|
textAlign?: string;
|
||||||
isDescendentOfSingleProductBlock: boolean;
|
|
||||||
isDescendentOfQueryLoop: boolean;
|
isDescendentOfQueryLoop: boolean;
|
||||||
postId: number;
|
postId: number;
|
||||||
productId: number;
|
productId: number;
|
||||||
|
@ -116,42 +28,29 @@ interface ProductRatingStarsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
|
export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
|
||||||
const { textAlign, shouldDisplayMockedReviewsWhenProductHasNoReviews } =
|
const {
|
||||||
props;
|
textAlign = '',
|
||||||
|
shouldDisplayMockedReviewsWhenProductHasNoReviews,
|
||||||
|
} = props;
|
||||||
const styleProps = useStyleProps( props );
|
const styleProps = useStyleProps( props );
|
||||||
const { parentClassName } = useInnerBlockLayoutContext();
|
const { parentClassName } = useInnerBlockLayoutContext();
|
||||||
const { product } = useProductDataContext();
|
const { product } = useProductDataContext();
|
||||||
const rating = getAverageRating( product );
|
const rating = getAverageRating( product );
|
||||||
const reviews = getRatingCount( product );
|
const reviews = getRatingCount( product );
|
||||||
|
const className = 'wc-block-components-product-rating-stars';
|
||||||
const className = clsx(
|
|
||||||
styleProps.className,
|
|
||||||
'wc-block-components-product-rating-stars',
|
|
||||||
{
|
|
||||||
[ `${ parentClassName }__product-rating` ]: parentClassName,
|
|
||||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const mockedRatings = shouldDisplayMockedReviewsWhenProductHasNoReviews ? (
|
|
||||||
<NoRating parentClassName={ parentClassName } />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const content = reviews ? (
|
|
||||||
<Rating
|
|
||||||
rating={ rating }
|
|
||||||
reviews={ reviews }
|
|
||||||
parentClassName={ parentClassName }
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
mockedRatings
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ className } style={ styleProps.style }>
|
<ProductRating
|
||||||
<div className="wc-block-components-product-rating-stars__container">
|
className={ className }
|
||||||
{ content }
|
showMockedReviews={
|
||||||
</div>
|
shouldDisplayMockedReviewsWhenProductHasNoReviews
|
||||||
</div>
|
}
|
||||||
|
styleProps={ styleProps }
|
||||||
|
parentClassName={ parentClassName }
|
||||||
|
reviews={ reviews }
|
||||||
|
rating={ rating }
|
||||||
|
textAlign={ textAlign }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,129 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import {
|
import {
|
||||||
useInnerBlockLayoutContext,
|
useInnerBlockLayoutContext,
|
||||||
useProductDataContext,
|
useProductDataContext,
|
||||||
} from '@woocommerce/shared-context';
|
} from '@woocommerce/shared-context';
|
||||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||||
import { isNumber, ProductResponseItem } from '@woocommerce/types';
|
import {
|
||||||
|
ProductRating,
|
||||||
|
getAverageRating,
|
||||||
|
getRatingCount,
|
||||||
|
} from '@woocommerce/editor-components/product-rating';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
type RatingProps = {
|
|
||||||
reviews: number;
|
|
||||||
rating: number;
|
|
||||||
parentClassName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAverageRating = (
|
|
||||||
product: Omit< ProductResponseItem, 'average_rating' > & {
|
|
||||||
average_rating: string;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const rating = parseFloat( product.average_rating );
|
|
||||||
|
|
||||||
return Number.isFinite( rating ) && rating > 0 ? rating : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRatingCount = ( product: ProductResponseItem ) => {
|
|
||||||
const count = isNumber( product.review_count )
|
|
||||||
? product.review_count
|
|
||||||
: parseInt( product.review_count, 10 );
|
|
||||||
|
|
||||||
return Number.isFinite( count ) && count > 0 ? count : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStarStyle = ( rating: number ) => ( {
|
|
||||||
width: ( rating / 5 ) * 100 + '%',
|
|
||||||
} );
|
|
||||||
|
|
||||||
const NoRating = ( { parentClassName }: { parentClassName: string } ) => {
|
|
||||||
const starStyle = getStarStyle( 0 );
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={ clsx(
|
|
||||||
'wc-block-components-product-rating__norating-container',
|
|
||||||
`${ parentClassName }-product-rating__norating-container`
|
|
||||||
) }
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={ 'wc-block-components-product-rating__norating' }
|
|
||||||
role="img"
|
|
||||||
>
|
|
||||||
<span style={ starStyle } />
|
|
||||||
</div>
|
|
||||||
<span>{ __( 'No Reviews', 'woocommerce' ) }</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Rating = ( props: RatingProps ): JSX.Element => {
|
|
||||||
const { rating, reviews, parentClassName } = props;
|
|
||||||
|
|
||||||
const starStyle = getStarStyle( rating );
|
|
||||||
|
|
||||||
const ratingText = sprintf(
|
|
||||||
/* translators: %f is referring to the average rating value */
|
|
||||||
__( 'Rated %f out of 5', 'woocommerce' ),
|
|
||||||
rating
|
|
||||||
);
|
|
||||||
|
|
||||||
const ratingHTML = {
|
|
||||||
__html: sprintf(
|
|
||||||
/* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */
|
|
||||||
_n(
|
|
||||||
'Rated %1$s out of 5 based on %2$s customer rating',
|
|
||||||
'Rated %1$s out of 5 based on %2$s customer ratings',
|
|
||||||
reviews,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
sprintf( '<strong class="rating">%f</strong>', rating ),
|
|
||||||
sprintf( '<span class="rating">%d</span>', reviews )
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={ clsx(
|
|
||||||
'wc-block-components-product-rating__stars',
|
|
||||||
`${ parentClassName }__product-rating__stars`
|
|
||||||
) }
|
|
||||||
role="img"
|
|
||||||
aria-label={ ratingText }
|
|
||||||
>
|
|
||||||
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReviewsCount = ( props: { reviews: number } ): JSX.Element => {
|
|
||||||
const { reviews } = props;
|
|
||||||
|
|
||||||
const reviewsCount = sprintf(
|
|
||||||
/* translators: %s is referring to the total of reviews for a product */
|
|
||||||
_n(
|
|
||||||
'(%s customer review)',
|
|
||||||
'(%s customer reviews)',
|
|
||||||
reviews,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
reviews
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="wc-block-components-product-rating__reviews_count">
|
|
||||||
{ reviewsCount }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProductRatingProps = {
|
type ProductRatingProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
textAlign?: string;
|
textAlign?: string;
|
||||||
|
@ -136,7 +30,7 @@ type ProductRatingProps = {
|
||||||
|
|
||||||
export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
|
export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
|
||||||
const {
|
const {
|
||||||
textAlign,
|
textAlign = '',
|
||||||
isDescendentOfSingleProductBlock,
|
isDescendentOfSingleProductBlock,
|
||||||
shouldDisplayMockedReviewsWhenProductHasNoReviews,
|
shouldDisplayMockedReviewsWhenProductHasNoReviews,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -146,38 +40,22 @@ export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
|
||||||
const rating = getAverageRating( product );
|
const rating = getAverageRating( product );
|
||||||
const reviews = getRatingCount( product );
|
const reviews = getRatingCount( product );
|
||||||
|
|
||||||
const className = clsx(
|
const className = 'wc-block-components-product-rating';
|
||||||
styleProps.className,
|
|
||||||
'wc-block-components-product-rating',
|
|
||||||
{
|
|
||||||
[ `${ parentClassName }__product-rating` ]: parentClassName,
|
|
||||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const mockedRatings = shouldDisplayMockedReviewsWhenProductHasNoReviews ? (
|
|
||||||
<NoRating parentClassName={ parentClassName } />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const content = reviews ? (
|
|
||||||
<Rating
|
|
||||||
rating={ rating }
|
|
||||||
reviews={ reviews }
|
|
||||||
parentClassName={ parentClassName }
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
mockedRatings
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) {
|
if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) {
|
||||||
return (
|
return (
|
||||||
<div className={ className } style={ styleProps.style }>
|
<ProductRating
|
||||||
<div className="wc-block-components-product-rating__container">
|
className={ className }
|
||||||
{ content }
|
showReviewCount={ isDescendentOfSingleProductBlock }
|
||||||
{ reviews && isDescendentOfSingleProductBlock ? (
|
showMockedReviews={
|
||||||
<ReviewsCount reviews={ reviews } />
|
shouldDisplayMockedReviewsWhenProductHasNoReviews
|
||||||
) : null }
|
}
|
||||||
</div>
|
styleProps={ styleProps }
|
||||||
</div>
|
parentClassName={ parentClassName }
|
||||||
|
reviews={ reviews }
|
||||||
|
rating={ rating }
|
||||||
|
textAlign={ textAlign }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ValidatedTextInput } from '@woocommerce/blocks-components';
|
||||||
import { AddressFormValues, ContactFormValues } from '@woocommerce/settings';
|
import { AddressFormValues, ContactFormValues } from '@woocommerce/settings';
|
||||||
import { useState, Fragment, useCallback } from '@wordpress/element';
|
import { useState, Fragment, useCallback } from '@wordpress/element';
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import { Button } from '@ariakit/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -50,7 +51,8 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
|
render={ <span /> }
|
||||||
className={
|
className={
|
||||||
'wc-block-components-address-form__address_2-toggle'
|
'wc-block-components-address-form__address_2-toggle'
|
||||||
}
|
}
|
||||||
|
@ -61,7 +63,7 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( {
|
||||||
__( '+ Add %s', 'woocommerce' ),
|
__( '+ Add %s', 'woocommerce' ),
|
||||||
field.label.toLowerCase()
|
field.label.toLowerCase()
|
||||||
) }
|
) }
|
||||||
</button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
tabIndex={ -1 }
|
tabIndex={ -1 }
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: $input-background-dark;
|
background-color: $input-background-dark;
|
||||||
color: $input-text-dark;
|
color: $input-text-dark;
|
||||||
box-shadow: 0 0 0 2px $input-border-gray;
|
box-shadow: 0 0 0 2px $input-border-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,12 +40,16 @@
|
||||||
color: $input-text-active;
|
color: $input-text-active;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px inherit;
|
box-shadow: 0 0 0 2px $input-border-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-dark-controls & {
|
.has-dark-controls & {
|
||||||
color: $input-text-dark;
|
color: $input-text-dark;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2px $input-border-dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-error & {
|
.has-error & {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import { FormFieldsConfig, getSetting } from '@woocommerce/settings';
|
import { FormFieldsConfig, getSetting } from '@woocommerce/settings';
|
||||||
import { formatAddress } from '@woocommerce/blocks/checkout/utils';
|
import { formatAddress } from '@woocommerce/blocks/checkout/utils';
|
||||||
|
import { Button } from '@ariakit/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -82,7 +83,8 @@ const AddressCard = ( {
|
||||||
) }
|
) }
|
||||||
</address>
|
</address>
|
||||||
{ onEdit && (
|
{ onEdit && (
|
||||||
<button
|
<Button
|
||||||
|
render={ <span /> }
|
||||||
className="wc-block-components-address-card__edit"
|
className="wc-block-components-address-card__edit"
|
||||||
aria-controls={ target }
|
aria-controls={ target }
|
||||||
aria-expanded={ isExpanded }
|
aria-expanded={ isExpanded }
|
||||||
|
@ -94,7 +96,7 @@ const AddressCard = ( {
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{ __( 'Edit', 'woocommerce' ) }
|
{ __( 'Edit', 'woocommerce' ) }
|
||||||
</button>
|
</Button>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { isPackageRateCollectable } from '@woocommerce/base-utils';
|
import { isPackageRateCollectable } from '@woocommerce/base-utils';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
import { Button } from '@ariakit/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -18,7 +19,6 @@ import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared';
|
||||||
import type { minMaxPrices } from './shared';
|
import type { minMaxPrices } from './shared';
|
||||||
import { defaultLocalPickupText, defaultShippingText } from './constants';
|
import { defaultLocalPickupText, defaultShippingText } from './constants';
|
||||||
import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils';
|
import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils';
|
||||||
import Button from '../../../../base/components/button';
|
|
||||||
|
|
||||||
const SHIPPING_RATE_ERROR = {
|
const SHIPPING_RATE_ERROR = {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
|
@ -44,8 +44,8 @@ const LocalPickupSelector = ( {
|
||||||
} ) => {
|
} ) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
render={ <div /> }
|
||||||
role="radio"
|
role="radio"
|
||||||
removeTextWrap
|
|
||||||
onClick={ onClick }
|
onClick={ onClick }
|
||||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||||
'wc-block-checkout__shipping-method-option--selected':
|
'wc-block-checkout__shipping-method-option--selected':
|
||||||
|
@ -129,9 +129,9 @@ const ShippingSelector = ( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
render={ <div /> }
|
||||||
role="radio"
|
role="radio"
|
||||||
onClick={ onClick }
|
onClick={ onClick }
|
||||||
removeTextWrap
|
|
||||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||||
'wc-block-checkout__shipping-method-option--selected':
|
'wc-block-checkout__shipping-method-option--selected':
|
||||||
checked === 'shipping',
|
checked === 'shipping',
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
useBlockProps,
|
useBlockProps,
|
||||||
RichText,
|
RichText,
|
||||||
} from '@wordpress/block-editor';
|
} from '@wordpress/block-editor';
|
||||||
import Button from '@woocommerce/base-components/button';
|
import { Button } from '@ariakit/react';
|
||||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||||
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
@ -53,12 +53,12 @@ const LocalPickupSelector = ( {
|
||||||
} ) => {
|
} ) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
render={ <div /> }
|
||||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||||
'wc-block-checkout__shipping-method-option--selected':
|
'wc-block-checkout__shipping-method-option--selected':
|
||||||
checked === 'pickup',
|
checked === 'pickup',
|
||||||
} ) }
|
} ) }
|
||||||
onClick={ onClick }
|
onClick={ onClick }
|
||||||
removeTextWrap
|
|
||||||
>
|
>
|
||||||
{ showIcon === true && (
|
{ showIcon === true && (
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -113,12 +113,12 @@ const ShippingSelector = ( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
render={ <div /> }
|
||||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||||
'wc-block-checkout__shipping-method-option--selected':
|
'wc-block-checkout__shipping-method-option--selected':
|
||||||
checked === 'shipping',
|
checked === 'shipping',
|
||||||
} ) }
|
} ) }
|
||||||
onClick={ onClick }
|
onClick={ onClick }
|
||||||
removeTextWrap
|
|
||||||
>
|
>
|
||||||
{ showIcon === true && (
|
{ showIcon === true && (
|
||||||
<Icon
|
<Icon
|
||||||
|
|
|
@ -9,22 +9,23 @@
|
||||||
// We have avoided nesting all the styles in case specificity changes introduce regressions elsewhere.
|
// We have avoided nesting all the styles in case specificity changes introduce regressions elsewhere.
|
||||||
.edit-post-visual-editor {
|
.edit-post-visual-editor {
|
||||||
.wc-block-checkout__shipping-method-container {
|
.wc-block-checkout__shipping-method-container {
|
||||||
.wc-block-components-button.wc-block-checkout__shipping-method-option {
|
.wc-block-checkout__shipping-method-option {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-post-visual-editor
|
.edit-post-visual-editor .wc-block-checkout__shipping-method-option,
|
||||||
.wc-block-components-button.wc-block-checkout__shipping-method-option,
|
.wc-block-checkout__shipping-method-option {
|
||||||
.wc-block-components-button.wc-block-checkout__shipping-method-option {
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
|
@ -35,8 +36,8 @@
|
||||||
outline: 1px solid $universal-border-light !important; // Overwriting Gutenberg styles
|
outline: 1px solid $universal-border-light !important; // Overwriting Gutenberg styles
|
||||||
border-radius: $universal-border-radius;
|
border-radius: $universal-border-radius;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&.components-button:hover:not(:disabled),
|
&:hover:not(:disabled),
|
||||||
&.components-button:focus:not(:disabled),
|
&:focus:not(:disabled),
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $universal-background;
|
background-color: $universal-background;
|
||||||
|
@ -65,6 +66,8 @@
|
||||||
|
|
||||||
.wc-block-checkout__shipping-method-option-price {
|
.wc-block-checkout__shipping-method-option-price {
|
||||||
@include font-size(small, 1rem);
|
@include font-size(small, 1rem);
|
||||||
|
flex-basis: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
em {
|
em {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
.wc-block-order-confirmation-status-description {
|
||||||
|
.woocommerce-verify-email {
|
||||||
|
margin-top: $gap-larger;
|
||||||
|
#verify-email {
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
@include breakpoint("<782px") {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wc-block-order-confirmation-status {
|
.wc-block-order-confirmation-status {
|
||||||
margin-top: $gap;
|
margin-top: $gap;
|
||||||
margin-bottom: $gap;
|
margin-bottom: $gap;
|
||||||
|
|
|
@ -36,12 +36,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.woocommerce-verify-email {
|
|
||||||
margin-top: $gap-larger;
|
|
||||||
#verify-email {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.has-background {
|
&.has-background {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason for using this component instead of the core/disabled component is
|
||||||
|
* that the Disabled component disrupts the focus on inner blocks. For example,
|
||||||
|
* when a heading block is nested inside, the text cursor, which indicates the
|
||||||
|
* editable area, isn't visible when focused on the heading block.
|
||||||
|
*
|
||||||
|
* This component only uses CSS to control the selected behavior of inner
|
||||||
|
* blocks, which fixes the abovementioned issues. However, being a static
|
||||||
|
* component comes with a limitation: this component is meant to be placed
|
||||||
|
* directly inside the block wrapper element that holds block props.
|
||||||
|
*/
|
||||||
|
export const InitialDisabled = ( {
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
} ): JSX.Element => (
|
||||||
|
<div className="wc-block-product-filter-components-initial-disabled">
|
||||||
|
<div className="wc-block-product-filter-components-initial-disabled-overlay" />
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
.wc-block-product-filter-components-initial-disabled {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.wc-block-product-filter-components-initial-disabled-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.is-selected > &,
|
||||||
|
.has-child-selected > & {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Icon } from '@wordpress/components';
|
||||||
|
import { info } from '@wordpress/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom notice component is designed specifically for new filter blocks. We
|
||||||
|
* are not reusing the existing components because we have a new design for the
|
||||||
|
* filter blocks notice. We want users to utilize the sidebar for attribute
|
||||||
|
* settings, so we are keeping the new notice minimal."
|
||||||
|
*/
|
||||||
|
export const Notice = ( { children }: { children: React.ReactNode } ) => (
|
||||||
|
<div className="wc-block-product-filter-components-notice">
|
||||||
|
<Icon
|
||||||
|
className="wc-block-product-filter-components-notice__icon"
|
||||||
|
icon={ info }
|
||||||
|
/>
|
||||||
|
<div className="wc-block-product-filter-components-notice__content">
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
.wc-block-product-filter-components-notice {
|
||||||
|
display: flex;
|
||||||
|
padding: $gap;
|
||||||
|
gap: $gap-smaller;
|
||||||
|
border: 1px solid;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
fill: $alert-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
> * {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,3 +3,9 @@ export const BlockOverlayAttribute = {
|
||||||
MOBILE: 'mobile',
|
MOBILE: 'mobile',
|
||||||
ALWAYS: 'always',
|
ALWAYS: 'always',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const EXCLUDED_BLOCKS = [
|
||||||
|
'woocommerce/product-filter-attribute',
|
||||||
|
'woocommerce/product-collection',
|
||||||
|
'core/query',
|
||||||
|
];
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
|
||||||
import {
|
import {
|
||||||
InnerBlocks,
|
InnerBlocks,
|
||||||
InspectorControls,
|
InspectorControls,
|
||||||
|
@ -37,10 +36,6 @@ import './editor.scss';
|
||||||
import { type BlockAttributes } from './types';
|
import { type BlockAttributes } from './types';
|
||||||
import { BlockOverlayAttribute } from './constants';
|
import { BlockOverlayAttribute } from './constants';
|
||||||
|
|
||||||
const defaultAttribute = getSetting< AttributeSetting >(
|
|
||||||
'defaultProductFilterAttribute'
|
|
||||||
);
|
|
||||||
|
|
||||||
const TEMPLATE: InnerBlockTemplate[] = [
|
const TEMPLATE: InnerBlockTemplate[] = [
|
||||||
[
|
[
|
||||||
'core/heading',
|
'core/heading',
|
||||||
|
@ -50,42 +45,8 @@ const TEMPLATE: InnerBlockTemplate[] = [
|
||||||
content: __( 'Filters', 'woocommerce' ),
|
content: __( 'Filters', 'woocommerce' ),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[ 'woocommerce/product-filter-active' ],
|
||||||
'woocommerce/product-filter',
|
[ 'woocommerce/product-filter-attribute' ],
|
||||||
{
|
|
||||||
filterType: 'active-filters',
|
|
||||||
heading: __( 'Active', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'price-filter',
|
|
||||||
heading: __( 'Price', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'stock-filter',
|
|
||||||
heading: __( 'Status', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'attribute-filter',
|
|
||||||
heading: defaultAttribute.attribute_label,
|
|
||||||
attributeId: parseInt( defaultAttribute.attribute_id, 10 ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'rating-filter',
|
|
||||||
heading: __( 'Rating', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'core/buttons',
|
'core/buttons',
|
||||||
{ layout: { type: 'flex' } },
|
{ layout: { type: 'flex' } },
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { getContext as getContextFn, store } from '@woocommerce/interactivity';
|
import {
|
||||||
|
getContext as getContextFn,
|
||||||
|
store,
|
||||||
|
navigate as navigateFn,
|
||||||
|
} from '@woocommerce/interactivity';
|
||||||
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
|
||||||
export interface ProductFiltersContext {
|
export interface ProductFiltersContext {
|
||||||
isDialogOpen: boolean;
|
isDialogOpen: boolean;
|
||||||
|
@ -11,7 +16,7 @@ export interface ProductFiltersContext {
|
||||||
const getContext = ( ns?: string ) =>
|
const getContext = ( ns?: string ) =>
|
||||||
getContextFn< ProductFiltersContext >( ns );
|
getContextFn< ProductFiltersContext >( ns );
|
||||||
|
|
||||||
const productFilters = {
|
store( 'woocommerce/product-filters', {
|
||||||
state: {
|
state: {
|
||||||
isDialogOpen: () => {
|
isDialogOpen: () => {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
|
@ -36,8 +41,41 @@ const productFilters = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
callbacks: {},
|
callbacks: {},
|
||||||
};
|
} );
|
||||||
|
|
||||||
store( 'woocommerce/product-filters', productFilters );
|
const isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
||||||
|
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
||||||
|
const needsRefresh = getSetting< boolean >(
|
||||||
|
'needsRefreshForInteractivityAPI',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
export type ProductFilters = typeof productFilters;
|
export function navigate( href: string, options = {} ) {
|
||||||
|
/**
|
||||||
|
* We may need to reset the current page when changing filters.
|
||||||
|
* This is because the current page may not exist for this set
|
||||||
|
* of filters and will 404 when the user navigates to it.
|
||||||
|
*
|
||||||
|
* There are different pagination formats to consider, as documented here:
|
||||||
|
* https://github.com/WordPress/gutenberg/blob/317eb8f14c8e1b81bf56972cca2694be250580e3/packages/block-library/src/query-pagination-numbers/index.php#L22-L85
|
||||||
|
*/
|
||||||
|
const url = new URL( href );
|
||||||
|
// When pretty permalinks are enabled, the page number may be in the path name.
|
||||||
|
url.pathname = url.pathname.replace( /\/page\/[0-9]+/i, '' );
|
||||||
|
// When plain permalinks are enabled, the page number may be in the "paged" query parameter.
|
||||||
|
url.searchParams.delete( 'paged' );
|
||||||
|
// On posts and pages the page number will be in a query parameter that
|
||||||
|
// identifies which block we are paginating.
|
||||||
|
url.searchParams.forEach( ( _, key ) => {
|
||||||
|
if ( key.match( /^query(?:-[0-9]+)?-page$/ ) ) {
|
||||||
|
url.searchParams.delete( key );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
// Make sure to update the href with the changes.
|
||||||
|
href = url.href;
|
||||||
|
|
||||||
|
if ( needsRefresh || ( ! isBlockTheme && isProductArchive ) ) {
|
||||||
|
return ( window.location.href = href );
|
||||||
|
}
|
||||||
|
return navigateFn( href, options );
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
"name": "woocommerce/product-filter-active",
|
"name": "woocommerce/product-filter-active",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"title": "Filter Options",
|
"title": "Active (Experimental)",
|
||||||
"description": "Display the currently active filters.",
|
"description": "Display the currently active filters.",
|
||||||
"category": "woocommerce",
|
"category": "woocommerce",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -11,11 +11,10 @@
|
||||||
"textdomain": "woocommerce",
|
"textdomain": "woocommerce",
|
||||||
"apiVersion": 3,
|
"apiVersion": 3,
|
||||||
"ancestor": [
|
"ancestor": [
|
||||||
"woocommerce/product-filter"
|
"woocommerce/product-filters"
|
||||||
],
|
],
|
||||||
"supports": {
|
"supports": {
|
||||||
"interactivity": true,
|
"interactivity": true,
|
||||||
"inserter": false,
|
|
||||||
"color": {
|
"color": {
|
||||||
"text": true,
|
"text": true,
|
||||||
"background": false
|
"background": false
|
||||||
|
@ -27,7 +26,7 @@
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"displayStyle": {
|
"displayStyle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "list"
|
"default": "chips"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ import { store, getContext } from '@woocommerce/interactivity';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
type ActiveFiltersContext = {
|
type ActiveFiltersContext = {
|
||||||
queryId: number;
|
queryId: number;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { registerBlockType } from '@wordpress/blocks';
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
import { productFilterOptions } from '@woocommerce/icons';
|
import { productFilterActive } from '@woocommerce/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -14,7 +14,7 @@ import './style.scss';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
icon: productFilterOptions,
|
icon: productFilterActive,
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
"name": "woocommerce/product-filter-attribute",
|
"name": "woocommerce/product-filter-attribute",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"title": "Filter Options",
|
"title": "Attribute (Experimental)",
|
||||||
"description": "Enable customers to filter the product grid by selecting one or more attributes, such as color.",
|
"description": "Enable customers to filter the product grid by selecting one or more attributes, such as color.",
|
||||||
"category": "woocommerce",
|
"category": "woocommerce",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -11,11 +11,10 @@
|
||||||
"textdomain": "woocommerce",
|
"textdomain": "woocommerce",
|
||||||
"apiVersion": 3,
|
"apiVersion": 3,
|
||||||
"ancestor": [
|
"ancestor": [
|
||||||
"woocommerce/product-filter"
|
"woocommerce/product-filters"
|
||||||
],
|
],
|
||||||
"supports": {
|
"supports": {
|
||||||
"interactivity": true,
|
"interactivity": true,
|
||||||
"inserter": false,
|
|
||||||
"color": {
|
"color": {
|
||||||
"text": true,
|
"text": true,
|
||||||
"background": false,
|
"background": false,
|
||||||
|
@ -78,7 +77,7 @@
|
||||||
},
|
},
|
||||||
"displayStyle": {
|
"displayStyle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "list"
|
"default": "woocommerce/product-filter-checkbox-list"
|
||||||
},
|
},
|
||||||
"selectType": {
|
"selectType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -100,5 +99,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default":true
|
"default":true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"attributes": {
|
||||||
|
"isPreview": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import { Icon, chevronDown } from '@wordpress/icons';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { PreviewDropdown } from '../../components/preview-dropdown';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AttributeDropdown = ( { label }: Props ) => {
|
|
||||||
return (
|
|
||||||
<div className="wc-block-attribute-filter style-dropdown">
|
|
||||||
<PreviewDropdown
|
|
||||||
placeholder={ sprintf(
|
|
||||||
/* translators: %s attribute name. */
|
|
||||||
__( 'Select %s', 'woocommerce' ),
|
|
||||||
label
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
<Icon icon={ chevronDown } size={ 30 } />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,86 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { sort } from 'fast-sort';
|
|
||||||
import { __, sprintf, _n } from '@wordpress/i18n';
|
|
||||||
import { SearchListControl } from '@woocommerce/editor-components/search-list-control';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
|
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
|
||||||
|
|
||||||
type AttributeSelectControlsProps = {
|
|
||||||
isCompact: boolean;
|
|
||||||
setAttributeId: ( id: number ) => void;
|
|
||||||
attributeId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AttributeSelectControls = ( {
|
|
||||||
isCompact,
|
|
||||||
setAttributeId,
|
|
||||||
attributeId,
|
|
||||||
}: AttributeSelectControlsProps ) => {
|
|
||||||
const messages = {
|
|
||||||
clear: __( 'Clear selected attribute', 'woocommerce' ),
|
|
||||||
list: __( 'Product Attributes', 'woocommerce' ),
|
|
||||||
noItems: __(
|
|
||||||
"Your store doesn't have any product attributes.",
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
search: __( 'Search for a product attribute:', 'woocommerce' ),
|
|
||||||
selected: ( n: number ) =>
|
|
||||||
sprintf(
|
|
||||||
/* translators: %d is the number of attributes selected. */
|
|
||||||
_n(
|
|
||||||
'%d attribute selected',
|
|
||||||
'%d attributes selected',
|
|
||||||
n,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
n
|
|
||||||
),
|
|
||||||
updated: __(
|
|
||||||
'Product attribute search results updated.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const list = sort(
|
|
||||||
ATTRIBUTES.map( ( item ) => {
|
|
||||||
return {
|
|
||||||
id: parseInt( item.attribute_id, 10 ),
|
|
||||||
name: item.attribute_label,
|
|
||||||
};
|
|
||||||
} )
|
|
||||||
).asc( 'name' );
|
|
||||||
|
|
||||||
const onChange = ( selected: SearchListItem[] ) => {
|
|
||||||
if ( ! selected || ! selected.length ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedId = selected[ 0 ].id;
|
|
||||||
const productAttribute = ATTRIBUTES.find(
|
|
||||||
( attribute ) => attribute.attribute_id === selectedId.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! productAttribute || attributeId === selectedId ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttributeId( selectedId as number );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SearchListControl
|
|
||||||
className="woocommerce-product-attributes"
|
|
||||||
list={ list }
|
|
||||||
selected={ list.filter( ( { id } ) => id === attributeId ) }
|
|
||||||
onChange={ onChange }
|
|
||||||
messages={ messages }
|
|
||||||
isSingle
|
|
||||||
isCompact={ isCompact }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,68 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ideally, this component should belong to packages/interactivity-components.
|
|
||||||
* But we haven't export it as a packages so we place it here temporary.
|
|
||||||
*/
|
|
||||||
export const Preview = ( { items }: { items: string[] } ) => {
|
|
||||||
const threshold = 15;
|
|
||||||
const isLongList = items.length > threshold;
|
|
||||||
return (
|
|
||||||
<div className="wc-block-interactivity-components-checkbox-list">
|
|
||||||
<ul className="wc-block-interactivity-components-checkbox-list__list">
|
|
||||||
{ ( isLongList ? items.slice( 0, threshold ) : items ).map(
|
|
||||||
( item, index ) => (
|
|
||||||
<li
|
|
||||||
key={ index }
|
|
||||||
className="wc-block-interactivity-components-checkbox-list__item"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
htmlFor={ `interactive-checkbox-${ index }` }
|
|
||||||
className=" wc-block-interactivity-components-checkbox-list__label"
|
|
||||||
>
|
|
||||||
<span className="wc-block-interactive-components-checkbox-list__input-wrapper">
|
|
||||||
<span className="wc-block-interactivity-components-checkbox-list__input-wrapper">
|
|
||||||
<input
|
|
||||||
name={ `interactive-checkbox-${ index }` }
|
|
||||||
type="checkbox"
|
|
||||||
className="wc-block-interactivity-components-checkbox-list__input"
|
|
||||||
// Harded coded some checked items for styling purpose.
|
|
||||||
defaultChecked={ [
|
|
||||||
1, 3, 4,
|
|
||||||
].includes( index ) }
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
className="wc-block-interactivity-components-checkbox-list__mark"
|
|
||||||
viewBox="0 0 10 8"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M9.25 1.19922L3.75 6.69922L1 3.94922"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="wc-block-interactivity-components-checkbox-list__text">
|
|
||||||
{ item }
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
) }
|
|
||||||
</ul>
|
|
||||||
{ isLongList && (
|
|
||||||
<span className="wc-block-interactivity-components-checkbox-list__show-more">
|
|
||||||
<small>{ __( 'Show more…', 'woocommerce' ) }</small>
|
|
||||||
</span>
|
|
||||||
) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,55 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { Icon, category, external } from '@wordpress/icons';
|
|
||||||
import { getAdminLink } from '@woocommerce/settings';
|
|
||||||
import { Placeholder, Button } from '@wordpress/components';
|
|
||||||
|
|
||||||
export const AttributesPlaceholder = ( {
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
} ) => (
|
|
||||||
<Placeholder
|
|
||||||
className="wc-block-attribute-filter"
|
|
||||||
icon={ <Icon icon={ category } /> }
|
|
||||||
label={ __( 'Filter by Attribute', 'woocommerce' ) }
|
|
||||||
instructions={ __(
|
|
||||||
'Enable customers to filter the product grid by selecting one or more attributes, such as color.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</Placeholder>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NoAttributesPlaceholder = () => (
|
|
||||||
<AttributesPlaceholder>
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
"Attributes are needed for filtering your products. You haven't created any attributes yet.",
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
className="wc-block-attribute-filter__add-attribute-button"
|
|
||||||
variant="secondary"
|
|
||||||
href={ getAdminLink(
|
|
||||||
'edit.php?post_type=product&page=product_attributes'
|
|
||||||
) }
|
|
||||||
target="_top"
|
|
||||||
>
|
|
||||||
{ __( 'Add new attribute', 'woocommerce' ) + ' ' }
|
|
||||||
<Icon icon={ external } />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="wc-block-attribute-filter__read_more_button"
|
|
||||||
variant="tertiary"
|
|
||||||
href="https://woocommerce.com/document/managing-product-taxonomies/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{ __( 'Learn more', 'woocommerce' ) }
|
|
||||||
</Button>
|
|
||||||
</AttributesPlaceholder>
|
|
||||||
);
|
|
|
@ -5,49 +5,71 @@ import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
export const attributeOptionsPreview = [
|
export const attributeOptionsPreview = [
|
||||||
{
|
{
|
||||||
id: 23,
|
label: __( 'Blue', 'woocommerce' ),
|
||||||
name: __( 'Blue', 'woocommerce' ),
|
value: 'blue',
|
||||||
slug: 'blue',
|
rawData: {
|
||||||
attr_slug: 'blue',
|
id: 23,
|
||||||
description: '',
|
name: __( 'Blue', 'woocommerce' ),
|
||||||
parent: 0,
|
slug: 'blue',
|
||||||
count: 4,
|
attr_slug: 'blue',
|
||||||
|
description: '',
|
||||||
|
parent: 0,
|
||||||
|
count: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 29,
|
label: __( 'Gray', 'woocommerce' ),
|
||||||
name: __( 'Gray', 'woocommerce' ),
|
value: 'gray',
|
||||||
slug: 'gray',
|
selected: true,
|
||||||
attr_slug: 'gray',
|
rawData: {
|
||||||
description: '',
|
id: 29,
|
||||||
parent: 0,
|
name: __( 'Gray', 'woocommerce' ),
|
||||||
count: 3,
|
slug: 'gray',
|
||||||
|
attr_slug: 'gray',
|
||||||
|
description: '',
|
||||||
|
parent: 0,
|
||||||
|
count: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 24,
|
label: __( 'Green', 'woocommerce' ),
|
||||||
name: __( 'Green', 'woocommerce' ),
|
value: 'green',
|
||||||
slug: 'green',
|
rawData: {
|
||||||
attr_slug: 'green',
|
id: 24,
|
||||||
description: '',
|
name: __( 'Green', 'woocommerce' ),
|
||||||
parent: 0,
|
slug: 'green',
|
||||||
count: 3,
|
attr_slug: 'green',
|
||||||
|
description: '',
|
||||||
|
parent: 0,
|
||||||
|
count: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 25,
|
label: __( 'Red', 'woocommerce' ),
|
||||||
name: __( 'Red', 'woocommerce' ),
|
value: 'red',
|
||||||
slug: 'red',
|
selected: true,
|
||||||
attr_slug: 'red',
|
rawData: {
|
||||||
description: '',
|
id: 25,
|
||||||
parent: 0,
|
name: __( 'Red', 'woocommerce' ),
|
||||||
count: 4,
|
slug: 'red',
|
||||||
|
attr_slug: 'red',
|
||||||
|
description: '',
|
||||||
|
parent: 0,
|
||||||
|
count: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 30,
|
label: __( 'Yellow', 'woocommerce' ),
|
||||||
name: __( 'Yellow', 'woocommerce' ),
|
value: 'yellow',
|
||||||
slug: 'yellow',
|
rawData: {
|
||||||
attr_slug: 'yellow',
|
id: 30,
|
||||||
description: '',
|
name: __( 'Yellow', 'woocommerce' ),
|
||||||
parent: 0,
|
slug: 'yellow',
|
||||||
count: 1,
|
attr_slug: 'yellow',
|
||||||
|
description: '',
|
||||||
|
parent: 0,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -5,29 +5,34 @@ import {
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollectionData,
|
useCollectionData,
|
||||||
} from '@woocommerce/base-context/hooks';
|
} from '@woocommerce/base-context/hooks';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import {
|
import {
|
||||||
AttributeSetting,
|
AttributeSetting,
|
||||||
AttributeTerm,
|
AttributeTerm,
|
||||||
objectHasProp,
|
objectHasProp,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import { useBlockProps } from '@wordpress/block-editor';
|
import {
|
||||||
import { Disabled, Notice, withSpokenMessages } from '@wordpress/components';
|
useBlockProps,
|
||||||
import { useEffect, useState, useMemo } from '@wordpress/element';
|
useInnerBlocksProps,
|
||||||
|
BlockContextProvider,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import { withSpokenMessages } from '@wordpress/components';
|
||||||
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { AttributeDropdown } from './components/attribute-dropdown';
|
import { Inspector } from './inspector';
|
||||||
import { Preview as CheckboxListPreview } from './components/checkbox-list-editor';
|
|
||||||
import { Inspector } from './components/inspector';
|
|
||||||
import { NoAttributesPlaceholder } from './components/placeholder';
|
|
||||||
import { attributeOptionsPreview } from './constants';
|
import { attributeOptionsPreview } from './constants';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import { EditProps, isAttributeCounts } from './types';
|
import { EditProps, isAttributeCounts } from './types';
|
||||||
import { getAttributeFromId } from './utils';
|
import { getAttributeFromId } from './utils';
|
||||||
import './editor.scss';
|
import { getAllowedBlocks } from '../../utils';
|
||||||
|
import { EXCLUDED_BLOCKS } from '../../constants';
|
||||||
|
import { FilterOptionItem } from '../../types';
|
||||||
|
import { InitialDisabled } from '../../components/initial-disabled';
|
||||||
|
import { Notice } from '../../components/notice';
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
|
|
||||||
|
@ -47,8 +52,10 @@ const Edit = ( props: EditProps ) => {
|
||||||
const attributeObject = getAttributeFromId( attributeId );
|
const attributeObject = getAttributeFromId( attributeId );
|
||||||
|
|
||||||
const [ attributeOptions, setAttributeOptions ] = useState<
|
const [ attributeOptions, setAttributeOptions ] = useState<
|
||||||
AttributeTerm[]
|
FilterOptionItem[]
|
||||||
>( [] );
|
>( [] );
|
||||||
|
const [ isOptionsLoading, setIsOptionsLoading ] =
|
||||||
|
useState< boolean >( true );
|
||||||
|
|
||||||
const { results: attributeTerms, isLoading: isTermsLoading } =
|
const { results: attributeTerms, isLoading: isTermsLoading } =
|
||||||
useCollection< AttributeTerm >( {
|
useCollection< AttributeTerm >( {
|
||||||
|
@ -59,7 +66,7 @@ const Edit = ( props: EditProps ) => {
|
||||||
query: { orderby: 'menu_order', hide_empty: hideEmpty },
|
query: { orderby: 'menu_order', hide_empty: hideEmpty },
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const { results: filteredCounts, isLoading: isCountsLoading } =
|
const { results: filteredCounts, isLoading: isFilterCountsLoading } =
|
||||||
useCollectionData( {
|
useCollectionData( {
|
||||||
queryAttribute: {
|
queryAttribute: {
|
||||||
taxonomy: attributeObject?.taxonomy || '',
|
taxonomy: attributeObject?.taxonomy || '',
|
||||||
|
@ -69,90 +76,137 @@ const Edit = ( props: EditProps ) => {
|
||||||
isEditor: true,
|
isEditor: true,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const isLoading = isTermsLoading || isCountsLoading;
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
if ( isTermsLoading || isFilterCountsLoading ) return;
|
||||||
|
|
||||||
const termIdHasProducts =
|
const termIdHasProducts =
|
||||||
objectHasProp( filteredCounts, 'attribute_counts' ) &&
|
objectHasProp( filteredCounts, 'attribute_counts' ) &&
|
||||||
isAttributeCounts( filteredCounts.attribute_counts )
|
isAttributeCounts( filteredCounts.attribute_counts )
|
||||||
? filteredCounts.attribute_counts.map( ( term ) => term.term )
|
? filteredCounts.attribute_counts.map( ( term ) => term.term )
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if ( termIdHasProducts.length === 0 && hideEmpty )
|
if ( termIdHasProducts.length === 0 && hideEmpty ) {
|
||||||
return setAttributeOptions( [] );
|
setAttributeOptions( [] );
|
||||||
|
} else {
|
||||||
|
setAttributeOptions(
|
||||||
|
attributeTerms
|
||||||
|
.filter( ( term ) => {
|
||||||
|
if ( hideEmpty )
|
||||||
|
return termIdHasProducts.includes( term.id );
|
||||||
|
return true;
|
||||||
|
} )
|
||||||
|
.sort( ( a, b ) => {
|
||||||
|
switch ( sortOrder ) {
|
||||||
|
case 'name-asc':
|
||||||
|
return a.name > b.name ? 1 : -1;
|
||||||
|
case 'name-desc':
|
||||||
|
return a.name < b.name ? 1 : -1;
|
||||||
|
case 'count-asc':
|
||||||
|
return a.count > b.count ? 1 : -1;
|
||||||
|
case 'count-desc':
|
||||||
|
default:
|
||||||
|
return a.count < b.count ? 1 : -1;
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.map( ( term, index ) => ( {
|
||||||
|
label: showCounts
|
||||||
|
? `${ term.name } (${ term.count })`
|
||||||
|
: term.name,
|
||||||
|
value: term.id.toString(),
|
||||||
|
selected: index === 1,
|
||||||
|
rawData: term,
|
||||||
|
} ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setAttributeOptions(
|
setIsOptionsLoading( false );
|
||||||
attributeTerms
|
}, [
|
||||||
.filter( ( term ) => {
|
showCounts,
|
||||||
if ( hideEmpty )
|
attributeTerms,
|
||||||
return termIdHasProducts.includes( term.id );
|
filteredCounts,
|
||||||
return true;
|
sortOrder,
|
||||||
} )
|
hideEmpty,
|
||||||
.sort( ( a, b ) => {
|
isTermsLoading,
|
||||||
switch ( sortOrder ) {
|
isFilterCountsLoading,
|
||||||
case 'name-asc':
|
] );
|
||||||
return a.name > b.name ? 1 : -1;
|
|
||||||
case 'name-desc':
|
|
||||||
return a.name < b.name ? 1 : -1;
|
|
||||||
case 'count-asc':
|
|
||||||
return a.count > b.count ? 1 : -1;
|
|
||||||
case 'count-desc':
|
|
||||||
default:
|
|
||||||
return a.count < b.count ? 1 : -1;
|
|
||||||
}
|
|
||||||
} )
|
|
||||||
);
|
|
||||||
}, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] );
|
|
||||||
|
|
||||||
const Wrapper = ( { children }: { children: React.ReactNode } ) => (
|
const { children, ...innerBlocksProps } = useInnerBlocksProps(
|
||||||
<div { ...useBlockProps() }>
|
useBlockProps(),
|
||||||
<Inspector { ...props } />
|
{
|
||||||
{ children }
|
allowedBlocks: getAllowedBlocks( EXCLUDED_BLOCKS ),
|
||||||
</div>
|
template: [
|
||||||
|
[
|
||||||
|
'core/group',
|
||||||
|
{
|
||||||
|
layout: {
|
||||||
|
type: 'flex',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
name: __( 'Header', 'woocommerce' ),
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
spacing: {
|
||||||
|
blockGap: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'core/heading',
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
content:
|
||||||
|
attributeObject?.label ||
|
||||||
|
__( 'Attribute', 'woocommerce' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'woocommerce/product-filter-clear-button',
|
||||||
|
{
|
||||||
|
lock: {
|
||||||
|
remove: true,
|
||||||
|
move: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
displayStyle,
|
||||||
|
{
|
||||||
|
lock: {
|
||||||
|
remove: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadingState = useMemo( () => {
|
const isLoading =
|
||||||
return [ ...Array( 5 ) ].map( ( x, i ) => (
|
isTermsLoading || isFilterCountsLoading || isOptionsLoading;
|
||||||
<li
|
|
||||||
key={ i }
|
|
||||||
style={ {
|
|
||||||
/* stylelint-disable */
|
|
||||||
width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
|
|
||||||
</li>
|
|
||||||
) );
|
|
||||||
}, [] );
|
|
||||||
|
|
||||||
if ( isPreview ) {
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Disabled>
|
|
||||||
<CheckboxListPreview
|
|
||||||
items={ attributeOptionsPreview.map( ( term ) => {
|
|
||||||
if ( showCounts )
|
|
||||||
return `${ term.name } (${ term.count })`;
|
|
||||||
return term.name;
|
|
||||||
} ) }
|
|
||||||
/>
|
|
||||||
</Disabled>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block rendering starts.
|
|
||||||
if ( Object.keys( ATTRIBUTES ).length === 0 )
|
if ( Object.keys( ATTRIBUTES ).length === 0 )
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<NoAttributesPlaceholder />
|
<Inspector { ...props } />
|
||||||
</Wrapper>
|
<Notice>
|
||||||
|
<p>
|
||||||
|
{ __(
|
||||||
|
"Attributes are needed for filtering your products. You haven't created any attributes yet.",
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
</Notice>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( ! attributeId || ! attributeObject )
|
if ( ! attributeId || ! attributeObject )
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<Notice status="warning" isDismissible={ false }>
|
<Inspector { ...props } />
|
||||||
|
<Notice>
|
||||||
<p>
|
<p>
|
||||||
{ __(
|
{ __(
|
||||||
'Please select an attribute to use this filter!',
|
'Please select an attribute to use this filter!',
|
||||||
|
@ -160,22 +214,14 @@ const Edit = ( props: EditProps ) => {
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
</Notice>
|
</Notice>
|
||||||
</Wrapper>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( isLoading )
|
if ( ! isLoading && attributeTerms.length === 0 )
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<ul className="is-loading wp-block-woocommerce-product-filter-attribute__loading">
|
<Inspector { ...props } />
|
||||||
{ loadingState }
|
<Notice>
|
||||||
</ul>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( attributeTerms.length === 0 )
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Notice status="warning" isDismissible={ false }>
|
|
||||||
<p>
|
<p>
|
||||||
{ __(
|
{ __(
|
||||||
'There are no products with the selected attributes.',
|
'There are no products with the selected attributes.',
|
||||||
|
@ -183,30 +229,28 @@ const Edit = ( props: EditProps ) => {
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
</Notice>
|
</Notice>
|
||||||
</Wrapper>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<Disabled>
|
<Inspector { ...props } />
|
||||||
{ displayStyle === 'dropdown' ? (
|
<InitialDisabled>
|
||||||
<AttributeDropdown
|
<BlockContextProvider
|
||||||
label={
|
value={ {
|
||||||
attributeObject.label ||
|
filterData: {
|
||||||
__( 'attribute', 'woocommerce' )
|
items:
|
||||||
}
|
attributeOptions.length === 0 && isPreview
|
||||||
/>
|
? attributeOptionsPreview
|
||||||
) : (
|
: attributeOptions,
|
||||||
<CheckboxListPreview
|
isLoading,
|
||||||
items={ attributeOptions.map( ( term ) => {
|
},
|
||||||
if ( showCounts )
|
} }
|
||||||
return `${ term.name } (${ term.count })`;
|
>
|
||||||
return term.name;
|
{ children }
|
||||||
} ) }
|
</BlockContextProvider>
|
||||||
/>
|
</InitialDisabled>
|
||||||
) }
|
</div>
|
||||||
</Disabled>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
.wp-block-woocommerce-product-filter-attribute__loading {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
@include placeholder();
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import { HTMLElementEvent } from '@woocommerce/types';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
type AttributeFilterContext = {
|
type AttributeFilterContext = {
|
||||||
attributeSlug: string;
|
attributeSlug: string;
|
||||||
|
@ -106,5 +106,12 @@ store( 'woocommerce/product-filter-attribute', {
|
||||||
|
|
||||||
navigate( getUrl( selectedTerms, attributeSlug, queryType ) );
|
navigate( getUrl( selectedTerms, attributeSlug, queryType ) );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearFilters: () => {
|
||||||
|
const { attributeSlug, queryType } =
|
||||||
|
getContext< ActiveAttributeFilterContext >();
|
||||||
|
|
||||||
|
navigate( getUrl( [], attributeSlug, queryType ) );
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -2,18 +2,21 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
import { productFilterOptions } from '@woocommerce/icons';
|
import { productFilterAttribute } from '@woocommerce/icons';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
import { registerBlockType } from '@wordpress/blocks';
|
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
import { AttributeSetting } from '@woocommerce/types';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
import Save from './save';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
const defaultAttribute = getSetting< AttributeSetting >(
|
const defaultAttribute = getSetting< AttributeSetting >(
|
||||||
'defaultProductFilterAttribute'
|
'defaultProductFilterAttribute'
|
||||||
|
@ -21,7 +24,7 @@ if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterAttribute,
|
||||||
attributes: {
|
attributes: {
|
||||||
...metadata.attributes,
|
...metadata.attributes,
|
||||||
attributeId: {
|
attributeId: {
|
||||||
|
@ -29,5 +32,25 @@ if ( isExperimentalBlocksEnabled() ) {
|
||||||
default: parseInt( defaultAttribute.attribute_id, 10 ),
|
default: parseInt( defaultAttribute.attribute_id, 10 ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
save: Save,
|
||||||
|
variations: ATTRIBUTES.map( ( attribute, index ) => {
|
||||||
|
return {
|
||||||
|
name: `product-filter-attribute-${ attribute.attribute_name }`,
|
||||||
|
title: `${ attribute.attribute_label } (Experimental)`,
|
||||||
|
description: sprintf(
|
||||||
|
// translators: %s is the attribute label.
|
||||||
|
__(
|
||||||
|
`Enable customers to filter the product collection by selecting one or more %s attributes.`,
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
attribute.attribute_label
|
||||||
|
),
|
||||||
|
attributes: {
|
||||||
|
attributeId: parseInt( attribute.attribute_id, 10 ),
|
||||||
|
},
|
||||||
|
isActive: [ 'attributeId' ],
|
||||||
|
isDefault: index === 0,
|
||||||
|
};
|
||||||
|
} ),
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,9 @@ import { getSetting } from '@woocommerce/settings';
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
import { AttributeSetting } from '@woocommerce/types';
|
||||||
import { InspectorControls } from '@wordpress/block-editor';
|
import { InspectorControls } from '@wordpress/block-editor';
|
||||||
import { dispatch, useSelect } from '@wordpress/data';
|
import { dispatch, useSelect } from '@wordpress/data';
|
||||||
import { createInterpolateElement } from '@wordpress/element';
|
import { createInterpolateElement, useState } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Block, getBlockTypes, createBlock } from '@wordpress/blocks';
|
||||||
import {
|
import {
|
||||||
ComboboxControl,
|
ComboboxControl,
|
||||||
PanelBody,
|
PanelBody,
|
||||||
|
@ -23,12 +24,15 @@ import {
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { sortOrderOptions } from '../constants';
|
import { sortOrderOptions } from './constants';
|
||||||
import { BlockAttributes, EditProps } from '../types';
|
import { BlockAttributes, EditProps } from './types';
|
||||||
import { getAttributeFromId } from '../utils';
|
import { getAttributeFromId } from './utils';
|
||||||
|
import { getInnerBlockByName } from '../../utils';
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
|
|
||||||
|
let displayStyleOptions: Block[] = [];
|
||||||
|
|
||||||
export const Inspector = ( {
|
export const Inspector = ( {
|
||||||
clientId,
|
clientId,
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -43,47 +47,29 @@ export const Inspector = ( {
|
||||||
hideEmpty,
|
hideEmpty,
|
||||||
clearButton,
|
clearButton,
|
||||||
} = attributes;
|
} = attributes;
|
||||||
const { updateBlockAttributes } = dispatch( 'core/block-editor' );
|
const { updateBlockAttributes, insertBlock, replaceBlock } =
|
||||||
const { productFilterWrapperBlockId, productFilterWrapperHeadingBlockId } =
|
dispatch( 'core/block-editor' );
|
||||||
useSelect(
|
const filterBlock = useSelect(
|
||||||
( select ) => {
|
( select ) => {
|
||||||
if ( ! clientId )
|
return select( 'core/block-editor' ).getBlock( clientId );
|
||||||
return {
|
},
|
||||||
productFilterWrapperBlockId: undefined,
|
[ clientId ]
|
||||||
productFilterWrapperHeadingBlockId: undefined,
|
);
|
||||||
};
|
const [ displayStyleBlocksAttributes, setDisplayStyleBlocksAttributes ] =
|
||||||
|
useState< Record< string, unknown > >( {} );
|
||||||
|
|
||||||
const { getBlockParentsByBlockName, getBlock } =
|
const filterHeadingBlock = getInnerBlockByName(
|
||||||
select( 'core/block-editor' );
|
filterBlock,
|
||||||
|
'core/heading'
|
||||||
|
);
|
||||||
|
|
||||||
const parentBlocksByBlockName = getBlockParentsByBlockName(
|
if ( displayStyleOptions.length === 0 ) {
|
||||||
clientId,
|
displayStyleOptions = getBlockTypes().filter( ( blockType ) =>
|
||||||
'woocommerce/product-filter'
|
blockType.ancestor?.includes(
|
||||||
);
|
'woocommerce/product-filter-attribute'
|
||||||
|
)
|
||||||
if ( parentBlocksByBlockName.length === 0 )
|
|
||||||
return {
|
|
||||||
productFilterWrapperBlockId: undefined,
|
|
||||||
productFilterWrapperHeadingBlockId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parentBlockId = parentBlocksByBlockName[ 0 ];
|
|
||||||
|
|
||||||
const parentBlock = getBlock( parentBlockId );
|
|
||||||
const headerGroupBlock = parentBlock?.innerBlocks.find(
|
|
||||||
( block ) => block.name === 'core/group'
|
|
||||||
);
|
|
||||||
const headingBlock = headerGroupBlock?.innerBlocks.find(
|
|
||||||
( block ) => block.name === 'core/heading'
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
productFilterWrapperBlockId: parentBlockId,
|
|
||||||
productFilterWrapperHeadingBlockId: headingBlock?.clientId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[ clientId ]
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -102,17 +88,9 @@ export const Inspector = ( {
|
||||||
} );
|
} );
|
||||||
const attributeObject =
|
const attributeObject =
|
||||||
getAttributeFromId( numericId );
|
getAttributeFromId( numericId );
|
||||||
if ( productFilterWrapperBlockId ) {
|
if ( filterHeadingBlock ) {
|
||||||
updateBlockAttributes(
|
updateBlockAttributes(
|
||||||
productFilterWrapperBlockId,
|
filterHeadingBlock.clientId,
|
||||||
{
|
|
||||||
attributeId: numericId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ( productFilterWrapperHeadingBlockId ) {
|
|
||||||
updateBlockAttributes(
|
|
||||||
productFilterWrapperHeadingBlockId,
|
|
||||||
{
|
{
|
||||||
content:
|
content:
|
||||||
attributeObject?.label ??
|
attributeObject?.label ??
|
||||||
|
@ -188,17 +166,46 @@ export const Inspector = ( {
|
||||||
value={ displayStyle }
|
value={ displayStyle }
|
||||||
onChange={ (
|
onChange={ (
|
||||||
value: BlockAttributes[ 'displayStyle' ]
|
value: BlockAttributes[ 'displayStyle' ]
|
||||||
) => setAttributes( { displayStyle: value } ) }
|
) => {
|
||||||
|
if ( ! filterBlock ) return;
|
||||||
|
const currentStyleBlock = getInnerBlockByName(
|
||||||
|
filterBlock,
|
||||||
|
displayStyle
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( currentStyleBlock ) {
|
||||||
|
setDisplayStyleBlocksAttributes( {
|
||||||
|
...displayStyleBlocksAttributes,
|
||||||
|
[ displayStyle ]:
|
||||||
|
currentStyleBlock.attributes,
|
||||||
|
} );
|
||||||
|
replaceBlock(
|
||||||
|
currentStyleBlock.clientId,
|
||||||
|
createBlock(
|
||||||
|
value,
|
||||||
|
displayStyleBlocksAttributes[ value ] ||
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
insertBlock(
|
||||||
|
createBlock( value ),
|
||||||
|
filterBlock.innerBlocks.length,
|
||||||
|
filterBlock.clientId,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setAttributes( { displayStyle: value } );
|
||||||
|
} }
|
||||||
style={ { width: '100%' } }
|
style={ { width: '100%' } }
|
||||||
>
|
>
|
||||||
<ToggleGroupControlOption
|
{ displayStyleOptions.map( ( blockType ) => (
|
||||||
label={ __( 'List', 'woocommerce' ) }
|
<ToggleGroupControlOption
|
||||||
value="list"
|
key={ blockType.name }
|
||||||
/>
|
label={ blockType.title }
|
||||||
<ToggleGroupControlOption
|
value={ blockType.name }
|
||||||
label={ __( 'Chips', 'woocommerce' ) }
|
/>
|
||||||
value="chips"
|
) ) }
|
||||||
/>
|
|
||||||
</ToggleGroupControl>
|
</ToggleGroupControl>
|
||||||
<ToggleControl
|
<ToggleControl
|
||||||
label={ __( 'Product counts', 'woocommerce' ) }
|
label={ __( 'Product counts', 'woocommerce' ) }
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
const Save = () => {
|
||||||
|
const blockProps = useBlockProps.save();
|
||||||
|
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
|
||||||
|
return <div { ...innerBlocksProps } />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Save;
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
|
"name": "woocommerce/product-filter-checkbox-list",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "List",
|
||||||
|
"description": "Display a list of filter options.",
|
||||||
|
"category": "woocommerce",
|
||||||
|
"keywords": [
|
||||||
|
"WooCommerce"
|
||||||
|
],
|
||||||
|
"textdomain": "woocommerce",
|
||||||
|
"apiVersion": 3,
|
||||||
|
"ancestor": [
|
||||||
|
"woocommerce/product-filter-attribute"
|
||||||
|
],
|
||||||
|
"supports": {
|
||||||
|
"color": {
|
||||||
|
"enableContrastChecker": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usesContext": [
|
||||||
|
"filterData"
|
||||||
|
],
|
||||||
|
"attributes": {
|
||||||
|
"optionElementBorder": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"customOptionElementBorder": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"optionElementSelected": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"customOptionElementSelected": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"optionElement": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"customOptionElement": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon } from '@wordpress/components';
|
||||||
|
import { checkMark } from '@woocommerce/icons';
|
||||||
|
import { useMemo } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
useBlockProps,
|
||||||
|
withColors,
|
||||||
|
InspectorControls,
|
||||||
|
// @ts-expect-error - no types.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown,
|
||||||
|
// @ts-expect-error - no types.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
import './editor.scss';
|
||||||
|
import { EditProps } from './types';
|
||||||
|
|
||||||
|
const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
|
const {
|
||||||
|
clientId,
|
||||||
|
context,
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
optionElementBorder,
|
||||||
|
setOptionElementBorder,
|
||||||
|
optionElementSelected,
|
||||||
|
setOptionElementSelected,
|
||||||
|
optionElement,
|
||||||
|
setOptionElement,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
customOptionElementBorder,
|
||||||
|
customOptionElementSelected,
|
||||||
|
customOptionElement,
|
||||||
|
} = attributes;
|
||||||
|
const { filterData } = context;
|
||||||
|
const { isLoading, items } = filterData;
|
||||||
|
|
||||||
|
const colorGradientSettings = useMultipleOriginColorsAndGradients();
|
||||||
|
const blockProps = useBlockProps( {
|
||||||
|
className: clsx( 'wc-block-product-filter-checkbox-list', {
|
||||||
|
'is-loading': isLoading,
|
||||||
|
'has-option-element-border-color':
|
||||||
|
optionElementBorder.color || customOptionElementBorder,
|
||||||
|
'has-option-element-selected-color':
|
||||||
|
optionElementSelected.color || customOptionElementSelected,
|
||||||
|
'has-option-element-color':
|
||||||
|
optionElement.color || customOptionElement,
|
||||||
|
} ),
|
||||||
|
style: {
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-border':
|
||||||
|
optionElementBorder.color || customOptionElementBorder,
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-selected':
|
||||||
|
optionElementSelected.color || customOptionElementSelected,
|
||||||
|
'--wc-product-filter-checkbox-list-option-element':
|
||||||
|
optionElement.color || customOptionElement,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const loadingState = useMemo( () => {
|
||||||
|
return [ ...Array( 5 ) ].map( ( x, i ) => (
|
||||||
|
<li
|
||||||
|
key={ i }
|
||||||
|
style={ {
|
||||||
|
/* stylelint-disable */
|
||||||
|
width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
) );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
if ( ! items ) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = 15;
|
||||||
|
const isLongList = items.length > threshold;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div { ...blockProps }>
|
||||||
|
<ul className="wc-block-product-filter-checkbox-list__list">
|
||||||
|
{ isLoading && loadingState }
|
||||||
|
{ ! isLoading &&
|
||||||
|
( isLongList
|
||||||
|
? items.slice( 0, threshold )
|
||||||
|
: items
|
||||||
|
).map( ( item, index ) => (
|
||||||
|
<li
|
||||||
|
key={ index }
|
||||||
|
className="wc-block-product-filter-checkbox-list__item"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor={ `interactive-checkbox-${ index }` }
|
||||||
|
className=" wc-block-product-filter-checkbox-list__label"
|
||||||
|
>
|
||||||
|
<span className="wc-block-interactive-components-checkbox-list__input-wrapper">
|
||||||
|
<span className="wc-block-product-filter-checkbox-list__input-wrapper">
|
||||||
|
<input
|
||||||
|
name={ `interactive-checkbox-${ index }` }
|
||||||
|
type="checkbox"
|
||||||
|
className="wc-block-product-filter-checkbox-list__input"
|
||||||
|
defaultChecked={
|
||||||
|
!! item.selected
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
className="wc-block-product-filter-checkbox-list__mark"
|
||||||
|
icon={ checkMark }
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="wc-block-product-filter-checkbox-list__text">
|
||||||
|
{ item.label }
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
) ) }
|
||||||
|
</ul>
|
||||||
|
{ ! isLoading && isLongList && (
|
||||||
|
<span className="wc-block-product-filter-checkbox-list__show-more">
|
||||||
|
<small>{ __( 'Show more…', 'woocommerce' ) }</small>
|
||||||
|
</span>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
<InspectorControls group="color">
|
||||||
|
{ colorGradientSettings.hasColorsOrGradients && (
|
||||||
|
<ColorGradientSettingsDropdown
|
||||||
|
__experimentalIsRenderedInSidebar
|
||||||
|
settings={ [
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Option Element Border',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
optionElementBorder.color ||
|
||||||
|
customOptionElementBorder,
|
||||||
|
isShownByDefault: true,
|
||||||
|
enableAlpha: true,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setOptionElementBorder( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementBorder: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setOptionElementBorder( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementBorder: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Option Element (Selected)',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
optionElementSelected.color ||
|
||||||
|
customOptionElementSelected,
|
||||||
|
isShownByDefault: true,
|
||||||
|
enableAlpha: true,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setOptionElementSelected( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementSelected: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setOptionElementSelected( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementSelected: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Option Element', 'woocommerce' ),
|
||||||
|
colorValue:
|
||||||
|
optionElement.color || customOptionElement,
|
||||||
|
isShownByDefault: true,
|
||||||
|
enableAlpha: true,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setOptionElement( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElement: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setOptionElement( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElement: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] }
|
||||||
|
panelId={ clientId }
|
||||||
|
{ ...colorGradientSettings }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withColors( {
|
||||||
|
optionElementBorder: 'option-element-border',
|
||||||
|
optionElementSelected: 'option-element-border',
|
||||||
|
optionElement: 'option-element',
|
||||||
|
} )( Edit );
|
|
@ -0,0 +1,10 @@
|
||||||
|
.wc-block-product-filter-checkbox-list.is-loading {
|
||||||
|
.wc-block-product-filter-checkbox-list__list {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
@include placeholder();
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { getContext, store } from '@woocommerce/interactivity';
|
||||||
|
import { HTMLElementEvent } from '@woocommerce/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CheckboxListContext = {
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
checked: boolean;
|
||||||
|
}[];
|
||||||
|
showAll: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
store( 'woocommerce/product-filter-checkbox-list', {
|
||||||
|
actions: {
|
||||||
|
showAllItems: () => {
|
||||||
|
const context = getContext< CheckboxListContext >();
|
||||||
|
context.showAll = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectCheckboxItem: ( event: HTMLElementEvent< HTMLInputElement > ) => {
|
||||||
|
const context = getContext< CheckboxListContext >();
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
context.items = context.items.map( ( item ) => {
|
||||||
|
if ( item.value.toString() === value ) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
checked: ! item.checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
|
import { productFilterOptions } from '@woocommerce/icons';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import metadata from './block.json';
|
||||||
|
import Edit from './edit';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
registerBlockType( metadata, {
|
||||||
|
edit: Edit,
|
||||||
|
icon: productFilterOptions,
|
||||||
|
} );
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__list) {
|
||||||
|
list-style: none outside;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__item.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__label) {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__item .wc-block-product-filter-checkbox-list__label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__input-wrapper) {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__input-wrapper::before {
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.1;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
.has-option-element-color & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__input) {
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--wc-product-filter-checkbox-list-option-element-border, transparent);
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
font-size: inherit;
|
||||||
|
height: 1em;
|
||||||
|
margin: 0;
|
||||||
|
width: 1em;
|
||||||
|
background: var(--wc-product-filter-checkbox-list-option-element, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__input:focus {
|
||||||
|
outline-width: 1px;
|
||||||
|
outline-color: var(--wc-product-filter-checkbox-list-option-element-border, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__mark) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
height: 1em;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.2em;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 1em;
|
||||||
|
color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__show-more) {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__show-more.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { BlockEditProps } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { FilterBlockContext } from '../../types';
|
||||||
|
|
||||||
|
export type Color = {
|
||||||
|
slug: string;
|
||||||
|
class: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlockAttributes = {
|
||||||
|
className: string;
|
||||||
|
optionElementBorder: string;
|
||||||
|
customOptionElementBorder: string;
|
||||||
|
optionElementSelected: string;
|
||||||
|
customOptionElementSelected: string;
|
||||||
|
optionElement: string;
|
||||||
|
customOptionElement: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditProps = BlockEditProps< BlockAttributes > & {
|
||||||
|
context: FilterBlockContext;
|
||||||
|
optionElementBorder: Color;
|
||||||
|
setOptionElementBorder: ( value: string ) => void;
|
||||||
|
optionElementSelected: Color;
|
||||||
|
setOptionElementSelected: ( value: string ) => void;
|
||||||
|
optionElement: Color;
|
||||||
|
setOptionElement: ( value: string ) => void;
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
|
"name": "woocommerce/product-filter-chips",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Chips",
|
||||||
|
"description": "Display filter options as chips.",
|
||||||
|
"category": "woocommerce",
|
||||||
|
"keywords": [
|
||||||
|
"WooCommerce"
|
||||||
|
],
|
||||||
|
"textdomain": "woocommerce",
|
||||||
|
"apiVersion": 3,
|
||||||
|
"ancestor": [
|
||||||
|
"woocommerce/product-filter-attribute"
|
||||||
|
],
|
||||||
|
"supports": {},
|
||||||
|
"usesContext": [
|
||||||
|
"filterData",
|
||||||
|
"isParentSelected"
|
||||||
|
],
|
||||||
|
"attributes": {}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
const Edit = () => {
|
||||||
|
return <div { ...useBlockProps() }>These are chips.</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Edit;
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
|
import { productFilterOptions } from '@woocommerce/icons';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import metadata from './block.json';
|
||||||
|
import Edit from './edit';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
registerBlockType( metadata, {
|
||||||
|
edit: Edit,
|
||||||
|
icon: productFilterOptions,
|
||||||
|
} );
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
:where(.wc-block-product-filter-chips) {
|
||||||
|
// WIP
|
||||||
|
}
|
|
@ -6,13 +6,9 @@ import { BlockEditProps } from '@wordpress/blocks';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { BLOCK_NAME_MAP } from './constants';
|
|
||||||
|
|
||||||
export type FilterType = keyof typeof BLOCK_NAME_MAP;
|
|
||||||
|
|
||||||
export type BlockAttributes = {
|
export type BlockAttributes = {
|
||||||
filterType: FilterType;
|
className: string;
|
||||||
heading: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditProps = BlockEditProps< BlockAttributes >;
|
export type EditProps = BlockEditProps< BlockAttributes >;
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Logic in this file is unused and should be moved to product-fitlers block.
|
||||||
|
*
|
||||||
|
* @see https://github.com/woocommerce/woocommerce/issues/50868
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +11,7 @@ import { store, getContext } from '@woocommerce/interactivity';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
const getQueryParams = ( e: Event ) => {
|
const getQueryParams = ( e: Event ) => {
|
||||||
const filterNavContainer = ( e.target as HTMLElement )?.closest(
|
const filterNavContainer = ( e.target as HTMLElement )?.closest(
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { debounce } from '@woocommerce/base-utils';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
import type { PriceFilterContext, PriceFilterStore } from './types';
|
import type { PriceFilterContext, PriceFilterStore } from './types';
|
||||||
|
|
||||||
const getUrl = ( context: PriceFilterContext ) => {
|
const getUrl = ( context: PriceFilterContext ) => {
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import { BlockVariation } from '@wordpress/blocks';
|
|
||||||
import {
|
|
||||||
productFilterActive,
|
|
||||||
productFilterAttribute,
|
|
||||||
productFilterPrice,
|
|
||||||
productFilterRating,
|
|
||||||
productFilterStockStatus,
|
|
||||||
} from '@woocommerce/icons';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import { AttributeSetting, objectHasProp } from '@woocommerce/types';
|
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
|
||||||
|
|
||||||
const variations: BlockVariation[] = [
|
|
||||||
{
|
|
||||||
name: 'product-filter-active',
|
|
||||||
title: __( 'Active (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Display the currently active filters.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
heading: __( 'Active filters', 'woocommerce' ),
|
|
||||||
filterType: 'active-filters',
|
|
||||||
},
|
|
||||||
icon: productFilterActive,
|
|
||||||
isDefault: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product-filter-price',
|
|
||||||
title: __( 'Price (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by choosing a price range.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'price-filter',
|
|
||||||
heading: __( 'Price', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
icon: productFilterPrice,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product-filter-stock-status',
|
|
||||||
title: __( 'Status (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by stock status.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'stock-filter',
|
|
||||||
heading: __( 'Status', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
icon: productFilterStockStatus,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product-filter-rating',
|
|
||||||
title: __( 'Rating (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by rating.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'rating-filter',
|
|
||||||
heading: __( 'Rating', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
icon: productFilterRating,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
ATTRIBUTES.forEach( ( attribute ) => {
|
|
||||||
variations.push( {
|
|
||||||
name: `product-filter-attribute-${ attribute.attribute_name }`,
|
|
||||||
title: `${ attribute.attribute_label } (Experimental)`,
|
|
||||||
description: sprintf(
|
|
||||||
// translators: %s is the attribute label.
|
|
||||||
__(
|
|
||||||
`Enable customers to filter the product collection by selecting one or more %s attributes.`,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attribute.attribute_label
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'attribute-filter',
|
|
||||||
heading: attribute.attribute_label,
|
|
||||||
attributeId: parseInt( attribute.attribute_id, 10 ),
|
|
||||||
},
|
|
||||||
icon: productFilterAttribute,
|
|
||||||
// Can be `isActive: [ 'filterType', 'attributeId' ]`, but the API is available from 6.6.
|
|
||||||
isActive: ( blockAttributes, variationAttributes ) => {
|
|
||||||
return (
|
|
||||||
blockAttributes.filterType === variationAttributes.filterType &&
|
|
||||||
blockAttributes.attributeId === variationAttributes.attributeId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
variations.push( {
|
|
||||||
name: 'product-filter-attribute',
|
|
||||||
title: __( 'Attribute (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by selecting one or more attributes, such as color.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'attribute-filter',
|
|
||||||
heading: __( 'Attribute', 'woocommerce' ),
|
|
||||||
attributeId: 0,
|
|
||||||
},
|
|
||||||
icon: productFilterAttribute,
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add `isActive` function to all Product Filter block variations.
|
|
||||||
* `isActive` function is used to find a variation match from a created
|
|
||||||
* Block by providing its attributes.
|
|
||||||
*/
|
|
||||||
variations.forEach( ( variation ) => {
|
|
||||||
if ( ! objectHasProp( variation, 'isActive' ) ) {
|
|
||||||
// @ts-expect-error: `isActive` is currently typed wrong in `@wordpress/blocks`.
|
|
||||||
variation.isActive = [ 'filterType' ];
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
export const blockVariations = variations;
|
|
|
@ -1,47 +0,0 @@
|
||||||
{
|
|
||||||
"name": "woocommerce/product-filter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Product Filter (Experimental)",
|
|
||||||
"description": "A block that adds product filters to the product collection.",
|
|
||||||
"category": "woocommerce",
|
|
||||||
"keywords": [
|
|
||||||
"WooCommerce",
|
|
||||||
"Filters"
|
|
||||||
],
|
|
||||||
"textdomain": "woocommerce",
|
|
||||||
"supports": {
|
|
||||||
"html": false,
|
|
||||||
"reusable": false,
|
|
||||||
"inserter": true
|
|
||||||
},
|
|
||||||
"ancestor": [
|
|
||||||
"woocommerce/product-filters"
|
|
||||||
],
|
|
||||||
"usesContext": [
|
|
||||||
"query",
|
|
||||||
"queryId"
|
|
||||||
],
|
|
||||||
"attributes": {
|
|
||||||
"filterType": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"heading": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"isPreview": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"attributeId": {
|
|
||||||
"type": "number",
|
|
||||||
"default": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"attributes": {
|
|
||||||
"isPreview": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apiVersion": 3,
|
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import { Notice } from '@wordpress/components';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
const Warning = () => {
|
|
||||||
const isWidgetEditor = getSetting< boolean >( 'isWidgetEditor' );
|
|
||||||
if ( isWidgetEditor ) {
|
|
||||||
return (
|
|
||||||
<Notice status="info" isDismissible={ false }>
|
|
||||||
{ __(
|
|
||||||
'The widget area containing Collection Filters block needs to be placed on a product archive page for filters to function properly.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</Notice>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSiteEditor = getSetting< boolean >( 'isSiteEditor' );
|
|
||||||
if ( ! isSiteEditor ) {
|
|
||||||
return (
|
|
||||||
<Notice status="warning" isDismissible={ false }>
|
|
||||||
{ __(
|
|
||||||
'When added to a post or page, Collection Filters block needs to be nested inside a Product Collection block to function properly.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</Notice>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Warning;
|
|
|
@ -1,8 +0,0 @@
|
||||||
export const BLOCK_NAME_MAP = {
|
|
||||||
'active-filters': 'woocommerce/product-filter-active',
|
|
||||||
'price-filter': 'woocommerce/product-filter-price',
|
|
||||||
'stock-filter': 'woocommerce/product-filter-stock-status',
|
|
||||||
'rating-filter': 'woocommerce/product-filter-rating',
|
|
||||||
'attribute-filter': 'woocommerce/product-filter-attribute',
|
|
||||||
'clear-button': 'woocommerce/product-filter-clear-button',
|
|
||||||
};
|
|
|
@ -1,113 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
|
|
||||||
import { BlockEditProps } from '@wordpress/blocks';
|
|
||||||
import { useSelect } from '@wordpress/data';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import Warning from './components/warning';
|
|
||||||
import './editor.scss';
|
|
||||||
import { getAllowedBlocks } from './utils';
|
|
||||||
import { BLOCK_NAME_MAP } from './constants';
|
|
||||||
import type { FilterType } from './types';
|
|
||||||
|
|
||||||
const Edit = ( {
|
|
||||||
attributes,
|
|
||||||
clientId,
|
|
||||||
}: BlockEditProps< {
|
|
||||||
heading: string;
|
|
||||||
filterType: FilterType;
|
|
||||||
isPreview: boolean;
|
|
||||||
attributeId: number | undefined;
|
|
||||||
} > ) => {
|
|
||||||
const blockProps = useBlockProps();
|
|
||||||
|
|
||||||
const isNested = useSelect( ( select ) => {
|
|
||||||
const { getBlockParentsByBlockName } = select( 'core/block-editor' );
|
|
||||||
return !! getBlockParentsByBlockName(
|
|
||||||
clientId,
|
|
||||||
'woocommerce/product-collection'
|
|
||||||
).length;
|
|
||||||
} );
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav { ...blockProps }>
|
|
||||||
{ ! isNested && <Warning /> }
|
|
||||||
<InnerBlocks
|
|
||||||
allowedBlocks={ getAllowedBlocks( [
|
|
||||||
...Object.values( BLOCK_NAME_MAP ),
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
'woocommerce/filter-wrapper',
|
|
||||||
'woocommerce/product-collection',
|
|
||||||
'core/query',
|
|
||||||
] ) }
|
|
||||||
template={ [
|
|
||||||
/**
|
|
||||||
* We want to hide the clear filter button for active filters block
|
|
||||||
* as it has its own "clear all" button.
|
|
||||||
*/
|
|
||||||
attributes.filterType === 'active-filters'
|
|
||||||
? [
|
|
||||||
'core/heading',
|
|
||||||
{ level: 3, content: attributes.heading || '' },
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'core/group',
|
|
||||||
{
|
|
||||||
layout: {
|
|
||||||
type: 'flex',
|
|
||||||
flexWrap: 'nowrap',
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
name: __( 'Header', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
spacing: {
|
|
||||||
blockGap: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[
|
|
||||||
[
|
|
||||||
'core/heading',
|
|
||||||
{
|
|
||||||
level: 3,
|
|
||||||
content: attributes.heading || '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter-clear-button',
|
|
||||||
{
|
|
||||||
lock: {
|
|
||||||
remove: true,
|
|
||||||
move: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
BLOCK_NAME_MAP[ attributes.filterType ],
|
|
||||||
{
|
|
||||||
lock: {
|
|
||||||
remove: true,
|
|
||||||
},
|
|
||||||
isPreview: attributes.isPreview,
|
|
||||||
attributeId:
|
|
||||||
attributes.filterType === 'attribute-filter' &&
|
|
||||||
attributes.attributeId
|
|
||||||
? attributes.attributeId
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
] }
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Edit;
|
|
|
@ -1,5 +0,0 @@
|
||||||
.wp-block-woocommerce-collection-filters {
|
|
||||||
.components-notice {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { navigate as navigateFn } from '@woocommerce/interactivity';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
|
|
||||||
const isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
|
||||||
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
|
||||||
const needsRefresh = getSetting< boolean >(
|
|
||||||
'needsRefreshForInteractivityAPI',
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
export function navigate( href: string, options = {} ) {
|
|
||||||
/**
|
|
||||||
* We may need to reset the current page when changing filters.
|
|
||||||
* This is because the current page may not exist for this set
|
|
||||||
* of filters and will 404 when the user navigates to it.
|
|
||||||
*
|
|
||||||
* There are different pagination formats to consider, as documented here:
|
|
||||||
* https://github.com/WordPress/gutenberg/blob/317eb8f14c8e1b81bf56972cca2694be250580e3/packages/block-library/src/query-pagination-numbers/index.php#L22-L85
|
|
||||||
*/
|
|
||||||
const url = new URL( href );
|
|
||||||
// When pretty permalinks are enabled, the page number may be in the path name.
|
|
||||||
url.pathname = url.pathname.replace( /\/page\/[0-9]+/i, '' );
|
|
||||||
// When plain permalinks are enabled, the page number may be in the "paged" query parameter.
|
|
||||||
url.searchParams.delete( 'paged' );
|
|
||||||
// On posts and pages the page number will be in a query parameter that
|
|
||||||
// identifies which block we are paginating.
|
|
||||||
url.searchParams.forEach( ( _, key ) => {
|
|
||||||
if ( key.match( /^query(?:-[0-9]+)?-page$/ ) ) {
|
|
||||||
url.searchParams.delete( key );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
// Make sure to update the href with the changes.
|
|
||||||
href = url.href;
|
|
||||||
|
|
||||||
if ( needsRefresh || ( ! isBlockTheme && isProductArchive ) ) {
|
|
||||||
return ( window.location.href = href );
|
|
||||||
}
|
|
||||||
return navigateFn( href, options );
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
BlockInstance,
|
|
||||||
createBlock,
|
|
||||||
registerBlockType,
|
|
||||||
} from '@wordpress/blocks';
|
|
||||||
import { Icon, more } from '@wordpress/icons';
|
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import metadata from './block.json';
|
|
||||||
import edit from './edit';
|
|
||||||
import save from './save';
|
|
||||||
import { BLOCK_NAME_MAP } from './constants';
|
|
||||||
import { BlockAttributes } from './types';
|
|
||||||
import { blockVariations } from './block-variations';
|
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
|
||||||
registerBlockType( metadata, {
|
|
||||||
icon: {
|
|
||||||
src: (
|
|
||||||
<Icon
|
|
||||||
icon={ more }
|
|
||||||
className="wc-block-editor-components-block-icon"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
edit,
|
|
||||||
save,
|
|
||||||
variations: blockVariations,
|
|
||||||
transforms: {
|
|
||||||
from: [
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
blocks: [ 'woocommerce/filter-wrapper' ],
|
|
||||||
transform: (
|
|
||||||
attributes: BlockAttributes,
|
|
||||||
innerBlocks: BlockInstance[]
|
|
||||||
) => {
|
|
||||||
const newInnerBlocks: BlockInstance[] = [];
|
|
||||||
// Loop through inner blocks to preserve the block order.
|
|
||||||
innerBlocks.forEach( ( block ) => {
|
|
||||||
if (
|
|
||||||
block.name ===
|
|
||||||
`woocommerce/${ attributes.filterType }`
|
|
||||||
) {
|
|
||||||
newInnerBlocks.push(
|
|
||||||
createBlock(
|
|
||||||
BLOCK_NAME_MAP[ attributes.filterType ],
|
|
||||||
block.attributes
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( block.name === 'core/heading' ) {
|
|
||||||
newInnerBlocks.push( block );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
return createBlock(
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
attributes,
|
|
||||||
newInnerBlocks
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { InnerBlocks } from '@wordpress/block-editor';
|
|
||||||
|
|
||||||
export default function save() {
|
|
||||||
return <InnerBlocks.Content />;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { getBlockTypes } from '@wordpress/blocks';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of allowed block names excluding the disallowedBlocks array.
|
|
||||||
*
|
|
||||||
* @param disallowedBlocks Array of block names to disallow.
|
|
||||||
* @return Array of allowed block names.
|
|
||||||
*/
|
|
||||||
export const getAllowedBlocks = ( disallowedBlocks: string[] ) => {
|
|
||||||
const allBlocks = getBlockTypes();
|
|
||||||
|
|
||||||
return allBlocks
|
|
||||||
.map( ( block ) => block.name )
|
|
||||||
.filter( ( name ) => ! disallowedBlocks.includes( name ) );
|
|
||||||
};
|
|
|
@ -8,7 +8,7 @@ import { DropdownContext } from '@woocommerce/interactivity-components/dropdown'
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
function getUrl( filters: Array< string | null > ) {
|
function getUrl( filters: Array< string | null > ) {
|
||||||
filters = filters.filter( Boolean );
|
filters = filters.filter( Boolean );
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { CheckboxListContext } from '@woocommerce/interactivity-components/check
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
const getUrl = ( activeFilters: string ) => {
|
const getUrl = ( activeFilters: string ) => {
|
||||||
const url = new URL( window.location.href );
|
const url = new URL( window.location.href );
|
||||||
|
|
|
@ -7,8 +7,8 @@ export type BlockOverlayAttributeOptions =
|
||||||
( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ];
|
( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ];
|
||||||
|
|
||||||
export interface BlockAttributes {
|
export interface BlockAttributes {
|
||||||
productId?: string;
|
|
||||||
setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void;
|
setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void;
|
||||||
|
productId?: string;
|
||||||
overlay: BlockOverlayAttributeOptions;
|
overlay: BlockOverlayAttributeOptions;
|
||||||
overlayIcon:
|
overlayIcon:
|
||||||
| 'filter-icon-1'
|
| 'filter-icon-1'
|
||||||
|
@ -18,3 +18,23 @@ export interface BlockAttributes {
|
||||||
overlayButtonStyle: 'label-icon' | 'label' | 'icon';
|
overlayButtonStyle: 'label-icon' | 'label' | 'icon';
|
||||||
overlayIconSize?: number;
|
overlayIconSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FilterOptionItem = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
selected?: boolean;
|
||||||
|
rawData?: Record< string, unknown >;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterBlockContext = {
|
||||||
|
filterData: {
|
||||||
|
isLoading: boolean;
|
||||||
|
items?: FilterOptionItem[];
|
||||||
|
range?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
isParentSelected: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { BlockInstance, getBlockTypes } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of allowed block names excluding the disallowedBlocks array.
|
||||||
|
*
|
||||||
|
* @param disallowedBlocks Array of block names to disallow.
|
||||||
|
* @return Array of allowed block names.
|
||||||
|
*/
|
||||||
|
export const getAllowedBlocks = ( disallowedBlocks: string[] ) => {
|
||||||
|
const allBlocks = getBlockTypes();
|
||||||
|
|
||||||
|
return allBlocks
|
||||||
|
.map( ( block ) => block.name )
|
||||||
|
.filter( ( name ) => ! disallowedBlocks.includes( name ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInnerBlockByName = (
|
||||||
|
block: BlockInstance | null,
|
||||||
|
name: string
|
||||||
|
): BlockInstance | null => {
|
||||||
|
if ( ! block ) return null;
|
||||||
|
|
||||||
|
if ( block.innerBlocks.length === 0 ) return null;
|
||||||
|
|
||||||
|
for ( const innerBlock of block.innerBlocks ) {
|
||||||
|
if ( innerBlock.name === name ) return innerBlock;
|
||||||
|
const innerInnerBlock = getInnerBlockByName( innerBlock, name );
|
||||||
|
if ( innerInnerBlock ) return innerInnerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const SEARCH_BLOCK_NAME = 'core/search';
|
||||||
|
export const SEARCH_VARIATION_NAME = 'woocommerce/product-search';
|
||||||
|
|
||||||
|
export enum PositionOptions {
|
||||||
|
OUTSIDE = 'button-outside',
|
||||||
|
INSIDE = 'button-inside',
|
||||||
|
NO_BUTTON = 'no-button',
|
||||||
|
BUTTON_ONLY = 'button-only',
|
||||||
|
INPUT_AND_BUTTON = 'input-and-button',
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
import { store as blockEditorStore, Warning } from '@wordpress/block-editor';
|
import { store as blockEditorStore, Warning } from '@wordpress/block-editor';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
@ -9,6 +10,7 @@ import { Icon, search } from '@wordpress/icons';
|
||||||
import { getSettingWithCoercion } from '@woocommerce/settings';
|
import { getSettingWithCoercion } from '@woocommerce/settings';
|
||||||
import { isBoolean } from '@woocommerce/types';
|
import { isBoolean } from '@woocommerce/types';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
|
import type { Block as BlockType } from '@wordpress/blocks';
|
||||||
import {
|
import {
|
||||||
// @ts-ignore waiting for @types/wordpress__blocks update
|
// @ts-ignore waiting for @types/wordpress__blocks update
|
||||||
registerBlockVariation,
|
registerBlockVariation,
|
||||||
|
@ -21,8 +23,10 @@ import {
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
import { withProductSearchControls } from './inspector-controls';
|
||||||
import Block from './block';
|
import Block from './block';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
import { SEARCH_BLOCK_NAME, SEARCH_VARIATION_NAME } from './constants';
|
||||||
|
|
||||||
const isBlockVariationAvailable = getSettingWithCoercion(
|
const isBlockVariationAvailable = getSettingWithCoercion(
|
||||||
'isBlockVariationAvailable',
|
'isBlockVariationAvailable',
|
||||||
|
@ -71,6 +75,7 @@ const PRODUCT_SEARCH_ATTRIBUTES = {
|
||||||
query: {
|
query: {
|
||||||
post_type: 'product',
|
post_type: 'product',
|
||||||
},
|
},
|
||||||
|
namespace: SEARCH_VARIATION_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
|
const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
|
||||||
|
@ -115,7 +120,7 @@ const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerBlockType( 'woocommerce/product-search', {
|
registerBlockType( SEARCH_VARIATION_NAME, {
|
||||||
title: __( 'Product Search', 'woocommerce' ),
|
title: __( 'Product Search', 'woocommerce' ),
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
icon: {
|
icon: {
|
||||||
|
@ -146,7 +151,7 @@ registerBlockType( 'woocommerce/product-search', {
|
||||||
isMatch: ( { idBase, instance } ) =>
|
isMatch: ( { idBase, instance } ) =>
|
||||||
idBase === 'woocommerce_product_search' && !! instance?.raw,
|
idBase === 'woocommerce_product_search' && !! instance?.raw,
|
||||||
transform: ( { instance } ) =>
|
transform: ( { instance } ) =>
|
||||||
createBlock( 'woocommerce/product-search', {
|
createBlock( SEARCH_VARIATION_NAME, {
|
||||||
label:
|
label:
|
||||||
instance.raw.title ||
|
instance.raw.title ||
|
||||||
PRODUCT_SEARCH_ATTRIBUTES.label,
|
PRODUCT_SEARCH_ATTRIBUTES.label,
|
||||||
|
@ -172,9 +177,31 @@ registerBlockType( 'woocommerce/product-search', {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
function registerProductSearchNamespace( props: BlockType, blockName: string ) {
|
||||||
|
if ( blockName === 'core/search' ) {
|
||||||
|
// Gracefully handle if settings.attributes is undefined.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore -- We need this because `attributes` is marked as `readonly`
|
||||||
|
props.attributes = {
|
||||||
|
...props.attributes,
|
||||||
|
namespace: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
SEARCH_VARIATION_NAME,
|
||||||
|
registerProductSearchNamespace
|
||||||
|
);
|
||||||
|
|
||||||
if ( isBlockVariationAvailable ) {
|
if ( isBlockVariationAvailable ) {
|
||||||
registerBlockVariation( 'core/search', {
|
registerBlockVariation( 'core/search', {
|
||||||
name: 'woocommerce/product-search',
|
name: SEARCH_VARIATION_NAME,
|
||||||
title: __( 'Product Search', 'woocommerce' ),
|
title: __( 'Product Search', 'woocommerce' ),
|
||||||
icon: {
|
icon: {
|
||||||
src: (
|
src: (
|
||||||
|
@ -199,4 +226,9 @@ if ( isBlockVariationAvailable ) {
|
||||||
),
|
),
|
||||||
attributes: PRODUCT_SEARCH_ATTRIBUTES,
|
attributes: PRODUCT_SEARCH_ATTRIBUTES,
|
||||||
} );
|
} );
|
||||||
|
addFilter(
|
||||||
|
'editor.BlockEdit',
|
||||||
|
SEARCH_BLOCK_NAME,
|
||||||
|
withProductSearchControls
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { type ElementType, useEffect, useState } from '@wordpress/element';
|
||||||
|
import { EditorBlock } from '@woocommerce/types';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { InspectorControls } from '@wordpress/block-editor';
|
||||||
|
import {
|
||||||
|
PanelBody,
|
||||||
|
RadioControl,
|
||||||
|
ToggleControl,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalToggleGroupControl as ToggleGroupControl,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore - Ignoring because `__experimentalToggleGroupControlOption` is not yet in the type definitions.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getInputAndButtonOption,
|
||||||
|
getSelectedRadioControlOption,
|
||||||
|
isInputAndButtonOption,
|
||||||
|
isWooSearchBlockVariation,
|
||||||
|
} from './utils';
|
||||||
|
import { ButtonPositionProps, ProductSearchBlockProps } from './types';
|
||||||
|
import { PositionOptions } from './constants';
|
||||||
|
|
||||||
|
const ProductSearchControls = ( props: ProductSearchBlockProps ) => {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const { buttonPosition, buttonUseIcon, showLabel } = attributes;
|
||||||
|
const [ initialPosition, setInitialPosition ] =
|
||||||
|
useState< ButtonPositionProps >( buttonPosition );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if (
|
||||||
|
isInputAndButtonOption( buttonPosition ) &&
|
||||||
|
initialPosition !== buttonPosition
|
||||||
|
) {
|
||||||
|
setInitialPosition( buttonPosition );
|
||||||
|
}
|
||||||
|
}, [ buttonPosition ] );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InspectorControls group="styles">
|
||||||
|
<PanelBody title={ __( 'Styles', 'woocommerce' ) }>
|
||||||
|
<RadioControl
|
||||||
|
selected={ getSelectedRadioControlOption( buttonPosition ) }
|
||||||
|
options={ [
|
||||||
|
{
|
||||||
|
label: __( 'Input and button', 'woocommerce' ),
|
||||||
|
value: PositionOptions.INPUT_AND_BUTTON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Input only', 'woocommerce' ),
|
||||||
|
value: PositionOptions.NO_BUTTON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Button only', 'woocommerce' ),
|
||||||
|
value: PositionOptions.BUTTON_ONLY,
|
||||||
|
},
|
||||||
|
] }
|
||||||
|
onChange={ (
|
||||||
|
selected: Partial< ButtonPositionProps > &
|
||||||
|
PositionOptions.INPUT_AND_BUTTON
|
||||||
|
) => {
|
||||||
|
if ( selected !== PositionOptions.INPUT_AND_BUTTON ) {
|
||||||
|
setAttributes( {
|
||||||
|
buttonPosition: selected,
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
const newButtonPosition =
|
||||||
|
getInputAndButtonOption( initialPosition );
|
||||||
|
setAttributes( {
|
||||||
|
buttonPosition: newButtonPosition,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
{ buttonPosition !== PositionOptions.NO_BUTTON && (
|
||||||
|
<>
|
||||||
|
{ buttonPosition !== PositionOptions.BUTTON_ONLY && (
|
||||||
|
<ToggleGroupControl
|
||||||
|
label={ __( 'BUTTON POSITION', 'woocommerce' ) }
|
||||||
|
isBlock
|
||||||
|
onChange={ ( value: ButtonPositionProps ) => {
|
||||||
|
setAttributes( {
|
||||||
|
buttonPosition: value,
|
||||||
|
} );
|
||||||
|
} }
|
||||||
|
value={ getInputAndButtonOption(
|
||||||
|
buttonPosition
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
value={ PositionOptions.INSIDE }
|
||||||
|
label={ __( 'Inside', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
value={ PositionOptions.OUTSIDE }
|
||||||
|
label={ __( 'Outside', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
</ToggleGroupControl>
|
||||||
|
) }
|
||||||
|
<ToggleGroupControl
|
||||||
|
label={ __( 'BUTTON APPEARANCE', 'woocommerce' ) }
|
||||||
|
isBlock
|
||||||
|
onChange={ ( value: boolean ) => {
|
||||||
|
setAttributes( {
|
||||||
|
buttonUseIcon: value,
|
||||||
|
} );
|
||||||
|
} }
|
||||||
|
value={ buttonUseIcon }
|
||||||
|
>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
value={ false }
|
||||||
|
label={ __( 'Text', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
value={ true }
|
||||||
|
label={ __( 'Icon', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
</ToggleGroupControl>
|
||||||
|
</>
|
||||||
|
) }
|
||||||
|
<ToggleControl
|
||||||
|
label={ __( 'Show input label', 'woocommerce' ) }
|
||||||
|
checked={ showLabel }
|
||||||
|
onChange={ ( showInputLabel: boolean ) =>
|
||||||
|
setAttributes( {
|
||||||
|
showLabel: showInputLabel,
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PanelBody>
|
||||||
|
</InspectorControls>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withProductSearchControls =
|
||||||
|
< T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
|
||||||
|
( props: ProductSearchBlockProps ) => {
|
||||||
|
return isWooSearchBlockVariation( props ) ? (
|
||||||
|
<>
|
||||||
|
<ProductSearchControls { ...props } />
|
||||||
|
<BlockEdit { ...props } />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<BlockEdit { ...props } />
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import type { EditorBlock } from '@woocommerce/types';
|
||||||
|
|
||||||
|
export type ButtonPositionProps =
|
||||||
|
| 'button-outside'
|
||||||
|
| 'button-inside'
|
||||||
|
| 'no-button'
|
||||||
|
| 'button-only';
|
||||||
|
|
||||||
|
export interface SearchBlockAttributes {
|
||||||
|
buttonPosition: ButtonPositionProps;
|
||||||
|
buttonText?: string;
|
||||||
|
buttonUseIcon: boolean;
|
||||||
|
isSearchFieldHidden: boolean;
|
||||||
|
label?: string;
|
||||||
|
namespace?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
showLabel: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductSearchBlockProps = EditorBlock< SearchBlockAttributes >;
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
PositionOptions,
|
||||||
|
SEARCH_BLOCK_NAME,
|
||||||
|
SEARCH_VARIATION_NAME,
|
||||||
|
} from './constants';
|
||||||
|
import { ButtonPositionProps, ProductSearchBlockProps } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies if a block is a Search block variation from our conventions
|
||||||
|
*
|
||||||
|
* We are extending Gutenberg's core Search block with our variations, and
|
||||||
|
* also adding extra namespaced attributes. If those namespaced attributes
|
||||||
|
* are present, we can be fairly sure it is our own registered variation.
|
||||||
|
*
|
||||||
|
* @param {ProductSearchBlockProps} block - A WooCommerce block.
|
||||||
|
*/
|
||||||
|
export function isWooSearchBlockVariation( block: ProductSearchBlockProps ) {
|
||||||
|
return (
|
||||||
|
block.name === SEARCH_BLOCK_NAME &&
|
||||||
|
block.attributes?.namespace === SEARCH_VARIATION_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given button position is a valid option for input and button placement.
|
||||||
|
*
|
||||||
|
* The function verifies if the provided `buttonPosition` matches one of the predefined
|
||||||
|
* values for placing a button either inside or outside an input field.
|
||||||
|
*
|
||||||
|
* @param {string} buttonPosition - The position of the button to check.
|
||||||
|
*/
|
||||||
|
export function isInputAndButtonOption( buttonPosition: string ): boolean {
|
||||||
|
return (
|
||||||
|
buttonPosition === 'button-outside' ||
|
||||||
|
buttonPosition === 'button-inside'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the option for the selected button position
|
||||||
|
*
|
||||||
|
* Based on the provided `buttonPosition`, the function returns a predefined option
|
||||||
|
* if the position is valid for input and button placement. If the position is not
|
||||||
|
* one of the predefined options, it returns the original `buttonPosition`.
|
||||||
|
*
|
||||||
|
* @param {string} buttonPosition - The position of the button to evaluate.
|
||||||
|
*/
|
||||||
|
export function getSelectedRadioControlOption(
|
||||||
|
buttonPosition: string
|
||||||
|
): string {
|
||||||
|
if ( isInputAndButtonOption( buttonPosition ) ) {
|
||||||
|
return PositionOptions.INPUT_AND_BUTTON;
|
||||||
|
}
|
||||||
|
return buttonPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the appropriate option for input and button placement based on the given value
|
||||||
|
*
|
||||||
|
* This function checks if the provided `value` is a valid option for placing a button either
|
||||||
|
* inside or outside an input field. If the `value` is valid, it is returned as is. If the `value`
|
||||||
|
* is not valid, the function returns a default option.
|
||||||
|
*
|
||||||
|
* @param {ButtonPositionProps} value - The position of the button to evaluate.
|
||||||
|
*/
|
||||||
|
export function getInputAndButtonOption( value: ButtonPositionProps ) {
|
||||||
|
if ( isInputAndButtonOption( value ) ) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// The default value is 'inside' for input and button.
|
||||||
|
return PositionOptions.OUTSIDE;
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { isNumber, ProductResponseItem } from '@woocommerce/types';
|
||||||
|
|
||||||
|
type RatingProps = {
|
||||||
|
className: string;
|
||||||
|
reviews: number;
|
||||||
|
rating: number;
|
||||||
|
parentClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAverageRating = (
|
||||||
|
product: Omit< ProductResponseItem, 'average_rating' > & {
|
||||||
|
average_rating: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const rating = parseFloat( product.average_rating );
|
||||||
|
|
||||||
|
return Number.isFinite( rating ) && rating > 0 ? rating : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRatingCount = ( product: ProductResponseItem ) => {
|
||||||
|
const count = isNumber( product.review_count )
|
||||||
|
? product.review_count
|
||||||
|
: parseInt( product.review_count, 10 );
|
||||||
|
|
||||||
|
return Number.isFinite( count ) && count > 0 ? count : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStarStyle = ( rating: number ) => ( {
|
||||||
|
width: ( rating / 5 ) * 100 + '%',
|
||||||
|
} );
|
||||||
|
|
||||||
|
const NoRating = ( {
|
||||||
|
className,
|
||||||
|
parentClassName,
|
||||||
|
}: {
|
||||||
|
className: string;
|
||||||
|
parentClassName: string;
|
||||||
|
} ) => {
|
||||||
|
const starStyle = getStarStyle( 0 );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ clsx(
|
||||||
|
`${ className }__norating-container`,
|
||||||
|
`${ parentClassName }-product-rating__norating-container`
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<div className={ `${ className }__norating` } role="img">
|
||||||
|
<span style={ starStyle } />
|
||||||
|
</div>
|
||||||
|
<span>{ __( 'No Reviews', 'woocommerce' ) }</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Rating = ( props: RatingProps ): JSX.Element => {
|
||||||
|
const { className, rating, reviews, parentClassName } = props;
|
||||||
|
|
||||||
|
const starStyle = getStarStyle( rating );
|
||||||
|
|
||||||
|
const ratingText = sprintf(
|
||||||
|
/* translators: %f is referring to the average rating value */
|
||||||
|
__( 'Rated %f out of 5', 'woocommerce' ),
|
||||||
|
rating
|
||||||
|
);
|
||||||
|
|
||||||
|
const ratingHTML = {
|
||||||
|
__html: sprintf(
|
||||||
|
/* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */
|
||||||
|
_n(
|
||||||
|
'Rated %1$s out of 5 based on %2$s customer rating',
|
||||||
|
'Rated %1$s out of 5 based on %2$s customer ratings',
|
||||||
|
reviews,
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
sprintf( '<strong class="rating">%f</strong>', rating ),
|
||||||
|
sprintf( '<span class="rating">%d</span>', reviews )
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ clsx(
|
||||||
|
`${ className }__stars`,
|
||||||
|
`${ parentClassName }__product-rating__stars`
|
||||||
|
) }
|
||||||
|
role="img"
|
||||||
|
aria-label={ ratingText }
|
||||||
|
>
|
||||||
|
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReviewsCount = ( props: {
|
||||||
|
className: string;
|
||||||
|
reviews: number;
|
||||||
|
} ): JSX.Element => {
|
||||||
|
const { className, reviews } = props;
|
||||||
|
|
||||||
|
const reviewsCount = sprintf(
|
||||||
|
/* translators: %s is referring to the total of reviews for a product */
|
||||||
|
_n(
|
||||||
|
'(%s customer review)',
|
||||||
|
'(%s customer reviews)',
|
||||||
|
reviews,
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
reviews
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={ `${ className }__reviews_count` }>
|
||||||
|
{ reviewsCount }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProductRatingProps = {
|
||||||
|
className: string;
|
||||||
|
showReviewCount?: boolean;
|
||||||
|
showMockedReviews?: boolean;
|
||||||
|
parentClassName?: string;
|
||||||
|
rating: number;
|
||||||
|
reviews: number;
|
||||||
|
styleProps: {
|
||||||
|
className: string;
|
||||||
|
style: CSSProperties;
|
||||||
|
};
|
||||||
|
textAlign?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductRating = (
|
||||||
|
props: ProductRatingProps
|
||||||
|
): JSX.Element | null => {
|
||||||
|
const {
|
||||||
|
className = 'wc-block-components-product-rating',
|
||||||
|
showReviewCount,
|
||||||
|
showMockedReviews,
|
||||||
|
parentClassName = '',
|
||||||
|
rating,
|
||||||
|
reviews,
|
||||||
|
styleProps,
|
||||||
|
textAlign,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const wrapperClassName = clsx( styleProps.className, className, {
|
||||||
|
[ `${ parentClassName }__product-rating` ]: parentClassName,
|
||||||
|
[ `has-text-align-${ textAlign }` ]: textAlign,
|
||||||
|
} );
|
||||||
|
|
||||||
|
const mockedRatings = showMockedReviews && (
|
||||||
|
<NoRating className={ className } parentClassName={ parentClassName } />
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = reviews ? (
|
||||||
|
<Rating
|
||||||
|
className={ className }
|
||||||
|
rating={ rating }
|
||||||
|
reviews={ reviews }
|
||||||
|
parentClassName={ parentClassName }
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
mockedRatings
|
||||||
|
);
|
||||||
|
|
||||||
|
const isReviewCountVisible = reviews && showReviewCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ wrapperClassName } style={ styleProps.style }>
|
||||||
|
<div className={ `${ className }__container` }>
|
||||||
|
{ content }
|
||||||
|
{ isReviewCountVisible ? (
|
||||||
|
<ReviewsCount className={ className } reviews={ reviews } />
|
||||||
|
) : null }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ export { default as bagAlt } from './library/bag-alt';
|
||||||
export { default as barcode } from './library/barcode';
|
export { default as barcode } from './library/barcode';
|
||||||
export { default as cart } from './library/cart';
|
export { default as cart } from './library/cart';
|
||||||
export { default as cartOutline } from './library/cart-outline';
|
export { default as cartOutline } from './library/cart-outline';
|
||||||
|
export { default as checkMark } from './library/check-mark';
|
||||||
export { default as checkPayment } from './library/check-payment';
|
export { default as checkPayment } from './library/check-payment';
|
||||||
export { default as closeSquareShadow } from './library/close-square-shadow';
|
export { default as closeSquareShadow } from './library/close-square-shadow';
|
||||||
export { default as customerAccount } from './library/customer-account';
|
export { default as customerAccount } from './library/customer-account';
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { IconProps } from '@wordpress/icons/build-types/icon';
|
||||||
|
import { Path, SVG } from '@wordpress/primitives';
|
||||||
|
|
||||||
|
const CheckMark = ( props: IconProps ) => (
|
||||||
|
<SVG
|
||||||
|
viewBox="0 0 10 8"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{ ...props }
|
||||||
|
>
|
||||||
|
<Path
|
||||||
|
d="M9.25 1.19922L3.75 6.69922L1 3.94922"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</SVG>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CheckMark;
|
|
@ -2,3 +2,4 @@
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './store-api-nonce';
|
import './store-api-nonce';
|
||||||
|
import './remove-user-locale';
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
|
import { addQueryArgs } from '@wordpress/url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { isStoreApiRequest } from './store-api-nonce';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to add the '_locale=site' query parameter from API requests.
|
||||||
|
*
|
||||||
|
* TODO: Remove once https://github.com/WordPress/gutenberg/issues/16805 is fixed and replace by removing userLocaleMiddleware middleware.
|
||||||
|
*
|
||||||
|
* @param {Object} options Fetch options.
|
||||||
|
* @param {Object} options.url The URL of the request.
|
||||||
|
* @param {Object} options.path The path of the request.
|
||||||
|
* @param {Function} next The next middleware or fetchHandler to call.
|
||||||
|
* @return {*} The evaluated result of the remaining middleware chain.
|
||||||
|
*/
|
||||||
|
const removeUserLocaleMiddleware = (
|
||||||
|
options: { url?: string; path?: string },
|
||||||
|
next: ( options: { url?: string; path?: string } ) => Promise< unknown >
|
||||||
|
): Promise< unknown > => {
|
||||||
|
if ( typeof options.url === 'string' && isStoreApiRequest( options ) ) {
|
||||||
|
options.url = addQueryArgs( options.url, { _locale: 'site' } );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof options.path === 'string' && isStoreApiRequest( options ) ) {
|
||||||
|
options.path = addQueryArgs( options.path, { _locale: 'site' } );
|
||||||
|
}
|
||||||
|
|
||||||
|
return next( options );
|
||||||
|
};
|
||||||
|
|
||||||
|
apiFetch.use( removeUserLocaleMiddleware );
|
|
@ -23,7 +23,7 @@ try {
|
||||||
*
|
*
|
||||||
* @return {boolean} Returns true if this is a store request.
|
* @return {boolean} Returns true if this is a store request.
|
||||||
*/
|
*/
|
||||||
const isStoreApiRequest = ( options ) => {
|
export const isStoreApiRequest = ( options ) => {
|
||||||
const url = options.url || options.path;
|
const url = options.url || options.path;
|
||||||
if ( ! url || ! options.method || options.method === 'GET' ) {
|
if ( ! url || ! options.method || options.method === 'GET' ) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -88,10 +88,6 @@ const blocks = {
|
||||||
'product-filters': {
|
'product-filters': {
|
||||||
isExperimental: true,
|
isExperimental: true,
|
||||||
},
|
},
|
||||||
'product-filter': {
|
|
||||||
isExperimental: true,
|
|
||||||
customDir: 'product-filters/inner-blocks/product-filter',
|
|
||||||
},
|
|
||||||
'product-filters-overlay': {
|
'product-filters-overlay': {
|
||||||
isExperimental: true,
|
isExperimental: true,
|
||||||
customDir: 'product-filters/inner-blocks/overlay',
|
customDir: 'product-filters/inner-blocks/overlay',
|
||||||
|
@ -124,6 +120,14 @@ const blocks = {
|
||||||
customDir: 'product-filters/inner-blocks/clear-button',
|
customDir: 'product-filters/inner-blocks/clear-button',
|
||||||
isExperimental: true,
|
isExperimental: true,
|
||||||
},
|
},
|
||||||
|
'product-filter-checkbox-list': {
|
||||||
|
customDir: 'product-filters/inner-blocks/checkbox-list',
|
||||||
|
isExperimental: true,
|
||||||
|
},
|
||||||
|
'product-filter-chips': {
|
||||||
|
customDir: 'product-filters/inner-blocks/chips',
|
||||||
|
isExperimental: true,
|
||||||
|
},
|
||||||
'order-confirmation-summary': {
|
'order-confirmation-summary': {
|
||||||
customDir: 'order-confirmation/summary',
|
customDir: 'order-confirmation/summary',
|
||||||
},
|
},
|
||||||
|
|
|
@ -256,7 +256,7 @@
|
||||||
"pnpm": "9.1.3"
|
"pnpm": "9.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ariakit/react": "^0.4.4",
|
"@ariakit/react": "^0.4.5",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/modifiers": "^6.0.1",
|
"@dnd-kit/modifiers": "^6.0.1",
|
||||||
"@dnd-kit/sortable": "^7.0.2",
|
"@dnd-kit/sortable": "^7.0.2",
|
||||||
|
@ -288,7 +288,7 @@
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-sort": "^3.4.0",
|
"fast-sort": "^3.4.0",
|
||||||
"html-react-parser": "3.0.4",
|
"html-react-parser": "3.0.4",
|
||||||
"postcode-validator": "3.8.15",
|
"postcode-validator": "3.9.2",
|
||||||
"preact": "^10.19.3",
|
"preact": "^10.19.3",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-number-format": "4.9.3",
|
"react-number-format": "4.9.3",
|
||||||
|
|
|
@ -13,6 +13,7 @@ const CUSTOM_REGEXES = new Map< string, RegExp >( [
|
||||||
[ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ],
|
[ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ],
|
||||||
[ 'KH', /^[0-9]{6}$/ ], // Cambodia (6-digit postal code).
|
[ 'KH', /^[0-9]{6}$/ ], // Cambodia (6-digit postal code).
|
||||||
[ 'LI', /^(94[8-9][0-9])$/ ],
|
[ 'LI', /^(94[8-9][0-9])$/ ],
|
||||||
|
[ 'MN', /^[0-9]{5}(-[0-9]{4})?$/ ], // Mongolia (5-digit postal code or 5-digit postal code followed by a hyphen and 4-digit postal code).
|
||||||
[ 'NI', /^[1-9]{1}[0-9]{4}$/ ], // Nicaragua (5-digit postal code)
|
[ 'NI', /^[1-9]{1}[0-9]{4}$/ ], // Nicaragua (5-digit postal code)
|
||||||
[ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ],
|
[ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ],
|
||||||
[ 'SI', /^([1-9][0-9]{3})$/ ],
|
[ 'SI', /^([1-9][0-9]{3})$/ ],
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue