Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Improve Products block Attributes Filter Inspector Controls #8583

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
534465c
Remove feature flag from Element variations
sunyatasattva Jan 25, 2023
eed60fb
Refactor `useProductAttributes` hook
sunyatasattva Feb 22, 2023
eb5e92c
Remove attribute-related types from PQ directory
sunyatasattva Feb 22, 2023
14e9373
Improve functionality of `SearchListControl`
sunyatasattva Feb 22, 2023
3be43c6
Enable removal of tokens from the input
sunyatasattva Feb 22, 2023
3860bd9
Improve styles:
sunyatasattva Feb 22, 2023
0efce7c
Correctly pass through class names to search item
sunyatasattva Feb 22, 2023
0b1cc7f
Enable keyboard navigation for collapsible items
sunyatasattva Feb 22, 2023
0f780d9
Add link to manage attributes
sunyatasattva Feb 22, 2023
5a90757
Change label inside the inspector controls
sunyatasattva Feb 22, 2023
f9c411e
Make search list attached when token type
sunyatasattva Feb 22, 2023
656a7b3
Implement more sophisticated behavior of parent checkbox
sunyatasattva Feb 23, 2023
48e7749
Fixed premature loading
sunyatasattva Feb 23, 2023
6e3490d
Hide the X from the placeholder when loading
sunyatasattva Feb 24, 2023
d87ae6d
Fix indeterminate checkmark behavior
sunyatasattva Feb 24, 2023
562c920
Fix TypeScript errors
sunyatasattva Feb 24, 2023
15d1d78
Fix `isSingle` usecases
sunyatasattva Feb 24, 2023
20b9242
Remove hardcoded `isSingle` from `expandableSearchListItem`
sunyatasattva Feb 28, 2023
6236c57
`item.children` can be `undefined`
sunyatasattva Feb 28, 2023
dd92db5
Make sure `item.children` is not undefined
sunyatasattva Feb 28, 2023
88f49f9
Fix TypeScript errors
sunyatasattva Feb 28, 2023
0dc46fb
Merge branch 'trunk' into improve/8135-pq-attribute-filter-inspector-…
sunyatasattva Mar 3, 2023
8344a88
Revert changes on AttributeFilter
sunyatasattva Mar 3, 2023
7d55176
Fix unit tests
sunyatasattva Mar 3, 2023
817ce9d
Fix front-end not correctly showing attributes
sunyatasattva Mar 6, 2023
2d6b30b
Fix array operations functions
sunyatasattva Mar 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion assets/js/base/context/hooks/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"../../../../../packages/checkout/index.js",
"../providers/cart-checkout/checkout-events/index.tsx",
"../providers/cart-checkout/payment-events/index.tsx",
"../providers/cart-checkout/shipping/index.js"
"../providers/cart-checkout/shipping/index.js",
"../../../editor-components/utils/*"
],
"exclude": [ "**/test/**" ]
}
79 changes: 79 additions & 0 deletions assets/js/base/context/hooks/use-product-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { useEffect, useRef, useState } from '@wordpress/element';
import { getAttributes, getTerms } from '@woocommerce/editor-components/utils';
import {
AttributeObject,
AttributeTerm,
AttributeWithTerms,
} from '@woocommerce/types';
import { formatError } from '@woocommerce/base-utils';

export default function useProductAttributes( shouldLoadAttributes: boolean ) {
const [ errorLoadingAttributes, setErrorLoadingAttributes ] =
useState< Awaited< ReturnType< typeof formatError > > | null >( null );
const [ isLoadingAttributes, setIsLoadingAttributes ] = useState( false );
const [ productsAttributes, setProductsAttributes ] = useState<
AttributeWithTerms[]
>( [] );
const hasLoadedAttributes = useRef( false );
nefeline marked this conversation as resolved.
Show resolved Hide resolved

useEffect( () => {
if (
! shouldLoadAttributes ||
isLoadingAttributes ||
hasLoadedAttributes.current
)
return;

async function fetchAttributesWithTerms() {
setIsLoadingAttributes( true );

try {
const attributes: AttributeObject[] = await getAttributes();
const attributesWithTerms: AttributeWithTerms[] = [];

for ( const attribute of attributes ) {
const terms: AttributeTerm[] = await getTerms(
attribute.id
);

attributesWithTerms.push( {
...attribute,
// Manually adding the parent id because of a Rest API bug
// returning always `0` as parent.
// see https://github.com/woocommerce/woocommerce-blocks/issues/8501
parent: 0,
terms: terms.map( ( term ) => ( {
...term,
attr_slug: attribute.taxonomy,
parent: attribute.id,
} ) ),
} );
}

setProductsAttributes( attributesWithTerms );
hasLoadedAttributes.current = true;
} catch ( e ) {
if ( e instanceof Error ) {
setErrorLoadingAttributes( await formatError( e ) );
}
} finally {
setIsLoadingAttributes( false );
}
}

fetchAttributesWithTerms();

return () => {
hasLoadedAttributes.current = true;
};
}, [ isLoadingAttributes, shouldLoadAttributes ] );

return {
errorLoadingAttributes,
isLoadingAttributes,
productsAttributes,
};
}
6 changes: 3 additions & 3 deletions assets/js/blocks/attribute-filter/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { sortBy } from 'lodash';
import { __, sprintf, _n } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import {
Expand All @@ -10,11 +11,10 @@ import {
} from '@wordpress/block-editor';
import { Icon, category, external } from '@wordpress/icons';
import { SearchListControl } from '@woocommerce/editor-components/search-list-control';
import { sortBy } from 'lodash';
import { getAdminLink, getSetting } from '@woocommerce/settings';
import BlockTitle from '@woocommerce/editor-components/block-title';
import classnames from 'classnames';
import { SearchListItemsType } from '@woocommerce/editor-components/search-list-control/types';
import { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
import { AttributeSetting } from '@woocommerce/types';
import {
Placeholder,
Expand Down Expand Up @@ -103,7 +103,7 @@ const Edit = ( {
);
};

const onChange = ( selected: SearchListItemsType ) => {
const onChange = ( selected: SearchListItem[] ) => {
if ( ! selected || ! selected.length ) {
return;
}
Expand Down
10 changes: 8 additions & 2 deletions assets/js/blocks/attribute-filter/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import Label from '@woocommerce/base-components/filter-element-label';
import { AttributeObject } from '@woocommerce/types';

export const previewOptions = [
{
Expand All @@ -27,9 +28,14 @@ export const previewOptions = [
},
];

export const previewAttributeObject = {
export const previewAttributeObject: AttributeObject = {
count: 0,
has_archives: true,
id: 0,
label: 'Preview',
name: 'preview',
order: 'menu_order',
parent: 0,
taxonomy: 'preview',
label: 'Preview',
type: '',
};
3 changes: 3 additions & 0 deletions assets/js/blocks/product-query/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
return rest;
}

export const EDIT_ATTRIBUTES_URL =
'/wp-admin/edit.php?post_type=product&page=product_attributes';

export const QUERY_LOOP_ID = 'core/query';

export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'taxQuery', 'search' ];
Expand Down
169 changes: 41 additions & 128 deletions assets/js/blocks/product-query/inspector-controls/attributes-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,155 +2,68 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import ProductAttributeTermControl from '@woocommerce/editor-components/product-attribute-term-control';
import {
FormTokenField,
ExternalLink,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import {
AttributeMetadata,
AttributeWithTerms,
ProductQueryBlock,
} from '../types';
import useProductAttributes from '../useProductAttributes';
import { ProductQueryBlock } from '../types';
import { setQueryAttribute } from '../utils';

function getAttributeMetadataFromToken(
token: string,
productsAttributes: AttributeWithTerms[]
) {
const [ attributeLabel, termName ] = token.split( ': ' );
const taxonomy = productsAttributes.find(
( attribute ) => attribute.attribute_label === attributeLabel
);

if ( ! taxonomy )
throw new Error( 'Product Query Filter: Invalid attribute label' );

const term = taxonomy.terms.find(
( currentTerm ) => currentTerm.name === termName
);

if ( ! term ) throw new Error( 'Product Query Filter: Invalid term name' );

return {
taxonomy: `pa_${ taxonomy.attribute_name }`,
termId: term.id,
};
}

function getAttributeFromMetadata(
metadata: AttributeMetadata,
productsAttributes: AttributeWithTerms[]
) {
const taxonomy = productsAttributes.find(
( attribute ) =>
attribute.attribute_name === metadata.taxonomy.slice( 3 )
);

return {
taxonomy,
term: taxonomy?.terms.find( ( term ) => term.id === metadata.termId ),
};
}

function getInputValueFromQueryParam(
queryParam: AttributeMetadata[] | undefined,
productAttributes: AttributeWithTerms[]
): FormTokenField.Value[] {
return (
queryParam?.map( ( metadata ) => {
const { taxonomy, term } = getAttributeFromMetadata(
metadata,
productAttributes
);

return ! taxonomy || ! term
? {
title: __(
'Saved taxonomy was perhaps deleted or the slug was changed.',
'woo-gutenberg-products-block'
),
value: __(
`Error with saved taxonomy`,
'woo-gutenberg-products-block'
),
status: 'error',
}
: `${ taxonomy.attribute_label }: ${ term.name }`;
} ) || []
);
}
import { EDIT_ATTRIBUTES_URL } from '../constants';

export const AttributesFilter = ( props: ProductQueryBlock ) => {
const { query } = props.attributes;
const { isLoadingAttributes, productsAttributes } =
useProductAttributes( true );

const attributesSuggestions = productsAttributes.reduce( ( acc, curr ) => {
const namespacedTerms = curr.terms.map(
( term ) => `${ curr.attribute_label }: ${ term.name }`
);

return [ ...acc, ...namespacedTerms ];
}, [] as string[] );
const [ selected, setSelected ] = useState< { id: number }[] >( [] );

useEffect( () => {
if ( query.__woocommerceAttributes ) {
setSelected(
query.__woocommerceAttributes.map( ( { termId: id } ) => ( {
id,
} ) )
);
}
}, [ query.__woocommerceAttributes ] );

return (
<ToolsPanelItem
label={ __( 'Product Attributes', 'woo-gutenberg-products-block' ) }
hasValue={ () => query.__woocommerceAttributes?.length }
>
<FormTokenField
disabled={ isLoadingAttributes }
label={ __(
'Product Attributes',
'woo-gutenberg-products-block'
) }
<ProductAttributeTermControl
messages={ {
search: __( 'Attributes', 'woo-gutenberg-products-block' ),
} }
selected={ selected }
onChange={ ( attributes ) => {
let __woocommerceAttributes;

try {
__woocommerceAttributes = attributes.map(
( attribute ) => {
attribute =
typeof attribute === 'string'
? attribute
: attribute.value;

return getAttributeMetadataFromToken(
attribute,
productsAttributes
);
}
);

setQueryAttribute( props, {
__woocommerceAttributes,
} );
} catch ( ok ) {
// Not required to do anything here
// Input validation is handled by the `validateInput`
// below, and we don't need to save anything.
}
const __woocommerceAttributes = attributes.map(
// eslint-disable-next-line @typescript-eslint/naming-convention
( { id, value } ) => ( {
termId: id,
taxonomy: value,
} )
);

setQueryAttribute( props, {
__woocommerceAttributes,
} );
} }
suggestions={ attributesSuggestions }
validateInput={ ( value: string ) =>
attributesSuggestions.includes( value )
}
value={
isLoadingAttributes
? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ]
: getInputValueFromQueryParam(
query.__woocommerceAttributes,
productsAttributes
)
}
__experimentalExpandOnFocus={ true }
operator={ 'any' }
isCompact={ true }
type={ 'token' }
/>
<ExternalLink
className="woocommerce-product-query-panel__external-link"
href={ EDIT_ATTRIBUTES_URL }
>
{ __( 'Manage attributes', 'woo-gutenberg-products-block' ) }
</ExternalLink>
</ToolsPanelItem>
);
};
9 changes: 9 additions & 0 deletions assets/js/blocks/product-query/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
grid-column: 1 / -1;
}
}

.woocommerce-product-query-panel__external-link {
display: block;
margin-top: $gap-small;

.components-external-link__icon {
margin-left: $gap-smaller;
}
}
13 changes: 1 addition & 12 deletions assets/js/blocks/product-query/types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
/**
* External dependencies
*/
import type {
AttributeSetting,
AttributeTerm,
EditorBlock,
} from '@woocommerce/types';

export interface AttributeMetadata {
taxonomy: string;
termId: number;
}

export type AttributeWithTerms = AttributeSetting & { terms: AttributeTerm[] };
import type { AttributeMetadata, EditorBlock } from '@woocommerce/types';

// The interface below disables the forbidden underscores
// naming convention because we are namespacing our
Expand Down
Loading