Skip to content

Commit

Permalink
A4A: Add Marketplace product search and filtering functionality. (#87707
Browse files Browse the repository at this point in the history
)

* Add Product search and filter component.

* Fix missing constant.

* Address PR comment.
  • Loading branch information
jkguidaven authored Feb 22, 2024
1 parent 187b987 commit c4c236f
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const PRODUCT_FILTER_ALL = '';
export const PRODUCT_FILTER_PLANS = 'plans';
export const PRODUCT_FILTER_PRODUCTS = 'products';
export const PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS = 'woocommerce-extensions';
export const PRODUCT_FILTER_VAULTPRESS_BACKUP_ADDONS = 'vaultpress-backup-addons';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// FIXME: Lets decide later if we need to move the calypso/jetpack-cloud imports to a shared common folder.
import { getQueryArg } from '@wordpress/url';
import { useTranslate } from 'i18n-calypso';
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import QueryProductsList from 'calypso/components/data/query-products-list';
import { parseQueryStringProducts } from 'calypso/jetpack-cloud/sections/partner-portal/lib/querystring-products';
import LicenseMultiProductCard from 'calypso/jetpack-cloud/sections/partner-portal/license-multi-product-card';
Expand All @@ -15,13 +15,15 @@ import useProductAndPlans from 'calypso/jetpack-cloud/sections/partner-portal/pr
import { useDispatch } from 'calypso/state';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import IssueLicenseContext from '../context';
import { PRODUCT_FILTER_ALL } from './constants';
import ProductFilterSearch from './product-filter-search';
import ProductFilterSelect from './product-filter-select';
import LicensesFormSection from './sections';
import type { SelectedLicenseProp } from '../types';
import type { SiteDetails } from '@automattic/data-stores';
import type { APIProductFamilyProduct } from 'calypso/state/partner-portal/types';

import './style.scss';

interface LicensesFormProps {
selectedSite?: SiteDetails | null;
suggestedProduct?: string;
Expand All @@ -38,6 +40,12 @@ export default function LicensesForm( {

const { selectedLicenses, setSelectedLicenses } = useContext( IssueLicenseContext );

const [ productSearchQuery, setProductSearchQuery ] = useState< string >( '' );

const [ selectedProductFilter, setSelectedProductFilter ] = useState< string | null >(
PRODUCT_FILTER_ALL
);

const {
filteredProductsAndBundles,
isLoadingProducts,
Expand All @@ -48,7 +56,9 @@ export default function LicensesForm( {
data,
} = useProductAndPlans( {
selectedSite,
selectedProductFilter,
selectedBundleSize: quantity,
productSearchQuery,
usePublicQuery: true, // FIXME: Fix this when we have the API endpoint for A4A
} );

Expand Down Expand Up @@ -188,6 +198,26 @@ export default function LicensesForm( {
[ quantity, selectedLicenses ]
);

const onProductFilterSelect = useCallback(
( value: string | null ) => {
setSelectedProductFilter( value );
dispatch(
recordTracksEvent( 'calypso_a4a_marketplace_issue_license_filter_submit', { value } )
);
},
[ dispatch ]
);

const onProductSearch = useCallback(
( value: string ) => {
setProductSearchQuery( value );
dispatch(
recordTracksEvent( 'calypso_a4a_marketplace_issue_license_search_submit', { value } )
);
},
[ dispatch ]
);

const onClickVariantOption = useCallback(
( product: APIProductFamilyProduct ) => {
dispatch(
Expand All @@ -199,6 +229,12 @@ export default function LicensesForm( {
[ dispatch ]
);

const trackClickCallback = useCallback(
( component: string ) => () =>
dispatch( recordTracksEvent( `calypso_a4a_marketplace_issue_license_${ component }_click` ) ),
[ dispatch ]
);

const isSingleLicenseView = quantity === 1;

const getProductCards = ( products: APIProductFamilyProduct[] ) => {
Expand Down Expand Up @@ -252,6 +288,19 @@ export default function LicensesForm( {
<div className="licenses-form">
<QueryProductsList currency="USD" />

<div className="licenses-form__actions">
<ProductFilterSearch
onProductSearch={ onProductSearch }
onClick={ trackClickCallback( 'search' ) }
/>
<ProductFilterSelect
selectedProductFilter={ selectedProductFilter }
onProductFilterSelect={ onProductFilterSelect }
onClick={ trackClickCallback( 'filter' ) }
isSingleLicense={ isSingleLicenseView }
/>
</div>

{ plans.length > 0 && (
<LicensesFormSection
title={ translate( 'Plans' ) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useTranslate } from 'i18n-calypso';
import Search from 'calypso/components/search';

type Props = {
onProductSearch: ( value: string ) => void;
onClick?: () => void;
};

export default function ProductFilterSearch( { onProductSearch, onClick }: Props ) {
const translate = useTranslate();

return (
<div className="licenses-form__product-filter-search">
<Search
onClick={ onClick }
onSearch={ onProductSearch }
placeholder={ translate( 'Search plans, products, add-ons, and extensions' ) }
compact
hideFocus
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { SelectDropdown } from '@automattic/components';
import { useTranslate } from 'i18n-calypso';
import { useCallback, useEffect, useMemo } from 'react';
import {
PRODUCT_FILTER_ALL,
PRODUCT_FILTER_PLANS,
PRODUCT_FILTER_PRODUCTS,
PRODUCT_FILTER_VAULTPRESS_BACKUP_ADDONS,
PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS,
} from './constants';

type Props = {
selectedProductFilter: string | null;
onClick?: () => void;
onProductFilterSelect: ( value: string | null ) => void;
isSingleLicense?: boolean;
};

type Option = {
value?: string | null;
label: string;
};

const getOptionByValue = ( options: Option[], value: string | null ): Option => {
return options.find( ( option ) => option.value === value ) ?? options[ 0 ];
};

export default function ProductFilterSelect( {
selectedProductFilter,
onClick,
onProductFilterSelect,
isSingleLicense,
}: Props ) {
const translate = useTranslate();

const productFilterOptions = useMemo< Option[] >( () => {
const options = [
{
value: PRODUCT_FILTER_ALL,
label: translate( 'All' ),
},
{
value: PRODUCT_FILTER_PLANS,
label: translate( 'Plans' ),
},
{
value: PRODUCT_FILTER_PRODUCTS,
label: translate( 'Products' ),
},
];

if ( isSingleLicense ) {
options.push(
{
value: PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS,
label: translate( 'WooCommerce Extensions' ),
},
{
value: PRODUCT_FILTER_VAULTPRESS_BACKUP_ADDONS,
label: translate( 'VaultPress Backup Add-ons' ),
}
);
}

return options;
}, [ isSingleLicense, translate ] );

const currentSelectedOption = getOptionByValue( productFilterOptions, selectedProductFilter );

const selectedText = translate( '{{b}}Products{{/b}}: %(productFilter)s', {
args: {
productFilter: currentSelectedOption.label as string,
},
components: {
b: <b />,
},
comment: 'productFilter is the selected filter type.',
} );

const onToggle = useCallback(
( { open }: { open: boolean } ) => {
if ( open ) {
onClick?.();
}
},
[ onClick ]
);

useEffect( () => {
if (
! isSingleLicense &&
( selectedProductFilter === PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS ||
selectedProductFilter === PRODUCT_FILTER_VAULTPRESS_BACKUP_ADDONS )
) {
onProductFilterSelect( PRODUCT_FILTER_ALL );
}
}, [ isSingleLicense, onProductFilterSelect, selectedProductFilter ] );

return (
<SelectDropdown
className="licenses-form__product-filter-select"
selectedText={ selectedText }
options={ productFilterOptions }
onToggle={ onToggle }
onSelect={ ( option: { value: string | null } ) => onProductFilterSelect( option.value ) }
compact
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,79 @@ p.licenses-form__description {
gap: 16px;
margin: 16px 0 32px;
}

.select-dropdown.is-compact.licenses-form__product-filter-select {
flex-basis: 100%;
height: 46px;

@include break-xlarge {
flex-basis: auto;
}

.select-dropdown__header {
height: 46px;
}

@include breakpoint-deprecated( ">660px" ) {
height: 35px;

.select-dropdown__header {
height: 35px;
}
}

.select-dropdown__header-text {
font-size: rem(13px);
font-weight: 400;
color: var(--color-text);
padding-inline-end: 8px;
}

.select-dropdown__header-text b {
color: var(--color-text);
}

.select-dropdown__container {
width: 100%;

@include break-xlarge {
width: auto;
}
}

.select-dropdown__item.is-selected {
background: var(--color-link-5);
color: var(--color-link-dark);
}

.select-dropdown__item:hover {
background: var(--color-link-5);
color: var(--color-neutral-90);
}
}

.licenses-form__product-filter-search {
flex-basis: 100%;

@include break-xlarge {
flex-basis: 360px;
}

.search {
&.is-open {
height: 46px;

@include breakpoint-deprecated( ">660px" ) {
height: 33px;
}
}
margin-block-end: 0;
border: 1px solid var(--color-neutral-10);
}

.search__input.form-text-input[type="search"] {
font-size: rem(13px);
font-weight: 400;
color: var(--color-text);
}
}

0 comments on commit c4c236f

Please sign in to comment.