/** * External dependencies */ import { resolveSelect, useSelect } from '@wordpress/data'; import { useState, useEffect, useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; type LocationType = 'product' | 'archive' | 'cart' | 'order' | 'generic'; type Context< T > = T & { templateSlug?: string; postId?: number; }; type SetEntityId = ( kind: 'postType' | 'taxonomy', name: 'product' | 'product_cat' | 'product_tag', slug: string, stateSetter: ( entityId: number | null ) => void ) => void; const templateSlugs = { singleProduct: 'single-product', productCategory: 'taxonomy-product_cat', productTag: 'taxonomy-product_tag', productAttribute: 'taxonomy-product_attribute', orderConfirmation: 'order-confirmation', cart: 'page-cart', checkout: 'page-checkout', }; const getIdFromResponse = ( resp?: Record< 'id', number >[] ): number | null => resp && resp.length && resp[ 0 ]?.id ? resp[ 0 ].id : null; const setEntityId: SetEntityId = async ( kind, name, slug, stateSetter ) => { const response = ( await resolveSelect( coreStore ).getEntityRecords( kind, name, { _fields: [ 'id' ], slug, } ) ) as Record< 'id', number >[]; const entityId = getIdFromResponse( response ); stateSetter( entityId ); }; const prepareGetEntitySlug = ( templateSlug: string ) => ( entitySlug: string ): string => templateSlug.replace( `${ entitySlug }-`, '' ); const prepareIsInSpecificTemplate = ( templateSlug: string ) => ( entitySlug: string ): boolean => templateSlug.includes( entitySlug ) && templateSlug !== entitySlug; const prepareIsInGenericTemplate = ( templateSlug: string ) => ( entitySlug: string ): boolean => templateSlug === entitySlug; const createLocationObject = ( type: LocationType, sourceData = {} ) => ( { type, sourceData, } ); type ContextProperties = { templateSlug: string; postId?: string; }; export const useGetLocation = < T, >( context: Context< T & ContextProperties >, clientId: string ) => { const templateSlug = context.templateSlug || ''; const postId = context.postId || null; const getEntitySlug = prepareGetEntitySlug( templateSlug ); const isInSpecificTemplate = prepareIsInSpecificTemplate( templateSlug ); // Detect Specific Templates const isInSpecificProductTemplate = isInSpecificTemplate( templateSlugs.singleProduct ); const isInSpecificCategoryTemplate = isInSpecificTemplate( templateSlugs.productCategory ); const isInSpecificTagTemplate = isInSpecificTemplate( templateSlugs.productTag ); const [ productId, setProductId ] = useState< number | null >( null ); const [ categoryId, setCategoryId ] = useState< number | null >( null ); const [ tagId, setTagId ] = useState< number | null >( null ); useEffect( () => { if ( isInSpecificProductTemplate ) { const slug = getEntitySlug( templateSlugs.singleProduct ); setEntityId( 'postType', 'product', slug, setProductId ); } if ( isInSpecificCategoryTemplate ) { const slug = getEntitySlug( templateSlugs.productCategory ); setEntityId( 'taxonomy', 'product_cat', slug, setCategoryId ); } if ( isInSpecificTagTemplate ) { const slug = getEntitySlug( templateSlugs.productTag ); setEntityId( 'taxonomy', 'product_tag', slug, setTagId ); } }, [ isInSpecificProductTemplate, isInSpecificCategoryTemplate, isInSpecificTagTemplate, getEntitySlug, ] ); const { isInSingleProductBlock, isInMiniCartBlock } = useSelect( ( select ) => ( { isInSingleProductBlock: // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this selector exist yet select( blockEditorStore ).getBlockParentsByBlockName( clientId, 'woocommerce/single-product' ).length > 0, isInMiniCartBlock: // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this selector exist yet select( blockEditorStore ).getBlockParentsByBlockName( clientId, 'woocommerce/mini-cart-contents' ).length > 0, } ), [ clientId ] ); /** * Case 1.1: SPECIFIC PRODUCT * Single Product block - take product ID from context */ if ( isInSingleProductBlock ) { return createLocationObject( 'product', { productId: postId } ); } /** * Case 1.2: SPECIFIC PRODUCT * Specific Single Product template - take product ID from taxononmy */ if ( isInSpecificProductTemplate ) { return createLocationObject( 'product', { productId } ); } const isInGenericTemplate = prepareIsInGenericTemplate( templateSlug ); /** * Case 1.3: GENERIC PRODUCT * Generic Single Product template */ const isInSingleProductTemplate = isInGenericTemplate( templateSlugs.singleProduct ); if ( isInSingleProductTemplate ) { return createLocationObject( 'product', { productId: null } ); } /** * Case 2.1: SPECIFIC TAXONOMY * Specific Category template - take category ID from */ if ( isInSpecificCategoryTemplate ) { return createLocationObject( 'archive', { taxonomy: 'product_cat', termId: categoryId, } ); } /** * Case 2.2: SPECIFIC TAXONOMY * Specific Tag template */ if ( isInSpecificTagTemplate ) { return createLocationObject( 'archive', { taxonomy: 'product_tag', termId: tagId, } ); } /** * Case 2.3: GENERIC TAXONOMY * Generic Taxonomy template */ const isInProductsByCategoryTemplate = isInGenericTemplate( templateSlugs.productCategory ); if ( isInProductsByCategoryTemplate ) { return createLocationObject( 'archive', { taxonomy: 'product_cat', termId: null, } ); } const isInProductsByTagTemplate = isInGenericTemplate( templateSlugs.productTag ); if ( isInProductsByTagTemplate ) { return createLocationObject( 'archive', { taxonomy: 'product_tag', termId: null, } ); } const isInProductsByAttributeTemplate = isInGenericTemplate( templateSlugs.productAttribute ); if ( isInProductsByAttributeTemplate ) { return createLocationObject( 'archive', { taxonomy: null, termId: null, } ); } /** * Case 3: GENERIC CART * Cart/Checkout templates or Mini Cart */ const isInCartContext = templateSlug === templateSlugs.cart || templateSlug === templateSlugs.checkout || isInMiniCartBlock; if ( isInCartContext ) { return createLocationObject( 'cart' ); } /** * Case 4: GENERIC ORDER * Order Confirmation template */ const isInOrderTemplate = isInGenericTemplate( templateSlugs.orderConfirmation ); if ( isInOrderTemplate ) { return createLocationObject( 'order' ); } /** * Case 5: GENERIC * All other cases */ return createLocationObject( 'generic' ); }; /** * In Product Collection block, queryContextIncludes attribute contains * list of attribute names that should be included in the query context. * * This hook returns the query context object based on the attribute names * provided in the queryContextIncludes array. * * Example: * { * clientID = 'd2c7e34f-70d6-417c-b582-f554a3a575f3', * queryContextIncludes = [ 'collection' ] * } * * The hook will return the following query context object: * { * collection: 'woocommerce/product-collection/featured' * } * * @param args Arguments for the hook. * @param args.clientId Client ID of the inner block. * @param args.queryContextIncludes Array of attribute names to be included in the query context. * * @return Query context object. */ export const useProductCollectionQueryContext = ( { clientId, queryContextIncludes, }: { clientId: string; queryContextIncludes: string[]; } ) => { const productCollectionBlockAttributes = useSelect( ( select ) => { const { getBlockParentsByBlockName, getBlockAttributes } = select( 'core/block-editor' ); const parentBlocksClientIds = getBlockParentsByBlockName( clientId, 'woocommerce/product-collection', true ); if ( parentBlocksClientIds?.length ) { const closestParentClientId = parentBlocksClientIds[ 0 ]; return getBlockAttributes( closestParentClientId ); } return null; }, [ clientId ] ); return useMemo( () => { // If the product collection block is not found, return null. if ( ! productCollectionBlockAttributes ) { return null; } const queryContext: { [ key: string ]: unknown; } = {}; if ( queryContextIncludes?.length ) { queryContextIncludes.forEach( ( attribute: string ) => { if ( productCollectionBlockAttributes?.[ attribute ] ) { queryContext[ attribute ] = productCollectionBlockAttributes[ attribute ]; } } ); } return queryContext; }, [ queryContextIncludes, productCollectionBlockAttributes ] ); };