rebase from live enviornment
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Returns the difference between two arrays (A - B)
|
||||
*/
|
||||
export function arrayDifferenceBy< T >( a: T[], b: T[], key: keyof T ) {
|
||||
const keys = new Set( b.map( ( item ) => item[ key ] ) );
|
||||
|
||||
return a.filter( ( item ) => ! keys.has( item[ key ] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of two arrays (A ∪ B)
|
||||
*/
|
||||
export function arrayUnionBy< T >( a: T[], b: T[], key: keyof T ) {
|
||||
const difference = arrayDifferenceBy( b, a, key );
|
||||
|
||||
return [ ...a, ...difference ];
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
AttributeObject,
|
||||
AttributeQuery,
|
||||
AttributeTerm,
|
||||
} from '@woocommerce/types';
|
||||
import { sort } from 'fast-sort';
|
||||
|
||||
/**
|
||||
* Given a query object, removes an attribute filter by a single slug.
|
||||
*
|
||||
* @param {Array} query Current query object.
|
||||
* @param {Function} setQuery Callback to update the current query object.
|
||||
* @param {Object} attribute An attribute object.
|
||||
* @param {string} slug Term slug to remove.
|
||||
*/
|
||||
export const removeAttributeFilterBySlug = (
|
||||
query: AttributeQuery[] = [],
|
||||
setQuery: ( query: AttributeQuery[] ) => void,
|
||||
attribute: AttributeObject,
|
||||
slug = ''
|
||||
) => {
|
||||
// Get current filter for provided attribute.
|
||||
const foundQuery = query.filter(
|
||||
( item ) => item.attribute === attribute.taxonomy
|
||||
);
|
||||
|
||||
const currentQuery = foundQuery.length ? foundQuery[ 0 ] : null;
|
||||
|
||||
if (
|
||||
! currentQuery ||
|
||||
! currentQuery.slug ||
|
||||
! Array.isArray( currentQuery.slug ) ||
|
||||
! currentQuery.slug.includes( slug )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSlugs = currentQuery.slug.filter( ( item ) => item !== slug );
|
||||
|
||||
// Remove current attribute filter from query.
|
||||
const returnQuery = query.filter(
|
||||
( item ) => item.attribute !== attribute.taxonomy
|
||||
);
|
||||
|
||||
// Add a new query for selected terms, if provided.
|
||||
if ( newSlugs.length > 0 ) {
|
||||
currentQuery.slug = newSlugs.sort();
|
||||
returnQuery.push( currentQuery );
|
||||
}
|
||||
|
||||
setQuery( sort( returnQuery ).asc( 'attribute' ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a query object, sets the query up to filter by a given attribute and attribute terms.
|
||||
*
|
||||
* @param {Array} query Current query object.
|
||||
* @param {Function} setQuery Callback to update the current query object.
|
||||
* @param {Object} attribute An attribute object.
|
||||
* @param {Array} attributeTerms Array of term objects.
|
||||
* @param {string} operator Operator for the filter. Valid values: in, and.
|
||||
*
|
||||
* @return {Object} An attribute object.
|
||||
*/
|
||||
export const updateAttributeFilter = (
|
||||
query: AttributeQuery[] = [],
|
||||
setQuery: ( query: AttributeQuery[] ) => void,
|
||||
attribute?: AttributeObject,
|
||||
attributeTerms: AttributeTerm[] = [],
|
||||
operator: 'in' | 'and' = 'in'
|
||||
) => {
|
||||
if ( ! attribute || ! attribute.taxonomy ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const returnQuery = query.filter(
|
||||
( item ) => item.attribute !== attribute.taxonomy
|
||||
);
|
||||
|
||||
if ( attributeTerms.length === 0 ) {
|
||||
setQuery( returnQuery );
|
||||
} else {
|
||||
returnQuery.push( {
|
||||
attribute: attribute.taxonomy,
|
||||
operator,
|
||||
slug: attributeTerms.map( ( { slug } ) => slug ).sort(),
|
||||
} );
|
||||
setQuery( sort( returnQuery ).asc( 'attribute' ) );
|
||||
}
|
||||
|
||||
return returnQuery;
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import {
|
||||
AttributeObject,
|
||||
AttributeSetting,
|
||||
AttributeTerm,
|
||||
AttributeWithTerms,
|
||||
isAttributeTerm,
|
||||
} from '@woocommerce/types';
|
||||
import { dispatch, select } from '@wordpress/data';
|
||||
|
||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||
|
||||
/**
|
||||
* Format an attribute from the settings into an object with standardized keys.
|
||||
*
|
||||
* @param {Object} attribute The attribute object.
|
||||
*/
|
||||
const attributeSettingToObject = ( attribute: AttributeSetting ) => {
|
||||
if ( ! attribute || ! attribute.attribute_name ) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: parseInt( attribute.attribute_id, 10 ),
|
||||
name: attribute.attribute_name,
|
||||
taxonomy: 'pa_' + attribute.attribute_name,
|
||||
label: attribute.attribute_label,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format all attribute settings into objects.
|
||||
*/
|
||||
const attributeObjects = ATTRIBUTES.reduce(
|
||||
( acc: Partial< AttributeObject >[], current ) => {
|
||||
const attributeObject = attributeSettingToObject( current );
|
||||
|
||||
if ( attributeObject && attributeObject.id ) {
|
||||
acc.push( attributeObject );
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Converts an Attribute object into a shape compatible with the `SearchListControl`
|
||||
*/
|
||||
export const convertAttributeObjectToSearchItem = (
|
||||
attribute: AttributeObject | AttributeTerm | AttributeWithTerms
|
||||
): SearchListItem => {
|
||||
const { count, id, name, parent } = attribute;
|
||||
|
||||
return {
|
||||
count,
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
breadcrumbs: [],
|
||||
children: [],
|
||||
value: isAttributeTerm( attribute ) ? attribute.attr_slug : '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get attribute data by taxonomy.
|
||||
*
|
||||
* @param {number} attributeId The attribute ID.
|
||||
* @return {Object|undefined} The attribute object if it exists.
|
||||
*/
|
||||
export const getAttributeFromID = ( attributeId: number ) => {
|
||||
if ( ! attributeId ) {
|
||||
return;
|
||||
}
|
||||
return attributeObjects.find( ( attribute ) => {
|
||||
return attribute.id === attributeId;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get attribute data by taxonomy.
|
||||
*
|
||||
* @param {string} taxonomy The attribute taxonomy name e.g. pa_color.
|
||||
* @return {Object|undefined} The attribute object if it exists.
|
||||
*/
|
||||
export const getAttributeFromTaxonomy = ( taxonomy: string ) => {
|
||||
if ( ! taxonomy ) {
|
||||
return;
|
||||
}
|
||||
return attributeObjects.find( ( attribute ) => {
|
||||
return attribute.taxonomy === taxonomy;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the taxonomy of an attribute by Attribute ID.
|
||||
*
|
||||
* @param {number} attributeId The attribute ID.
|
||||
* @return {string} The taxonomy name.
|
||||
*/
|
||||
export const getTaxonomyFromAttributeId = ( attributeId: number ) => {
|
||||
if ( ! attributeId ) {
|
||||
return null;
|
||||
}
|
||||
const attribute = getAttributeFromID( attributeId );
|
||||
return attribute ? attribute.taxonomy : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an attribute in a sibling block. Useful if two settings control the same attribute, but you don't want to
|
||||
* have this attribute exist on a parent block.
|
||||
*/
|
||||
export const updateAttributeInSiblingBlock = (
|
||||
clientId: string,
|
||||
attribute: string,
|
||||
newValue: unknown,
|
||||
siblingBlockName: string
|
||||
) => {
|
||||
const store = select( 'core/block-editor' );
|
||||
const actions = dispatch( 'core/block-editor' );
|
||||
const parentBlocks = store.getBlockParents( clientId );
|
||||
|
||||
let shippingMethodsBlockClientId = '';
|
||||
|
||||
// Loop through parent block's children until we find woocommerce/checkout-shipping-methods-block.
|
||||
// Also set this attribute in the woocommerce/checkout-shipping-methods-block.
|
||||
parentBlocks.forEach( ( parent ) => {
|
||||
const childBlock = store
|
||||
.getBlock( parent )
|
||||
.innerBlocks.find( ( child ) => child.name === siblingBlockName );
|
||||
if ( ! childBlock ) {
|
||||
return;
|
||||
}
|
||||
shippingMethodsBlockClientId = childBlock.clientId;
|
||||
} );
|
||||
actions.updateBlockAttributes( shippingMethodsBlockClientId, {
|
||||
[ attribute ]: newValue,
|
||||
} );
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getQueryArg, getQueryArgs, addQueryArgs } from '@wordpress/url';
|
||||
import { getSettingWithCoercion } from '@woocommerce/settings';
|
||||
import { isBoolean } from '@woocommerce/types';
|
||||
|
||||
const filteringForPhpTemplate = getSettingWithCoercion(
|
||||
'isRenderingPhpTemplate',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns specified parameter from URL
|
||||
*
|
||||
* @param {string} name Parameter you want the value of.
|
||||
*/
|
||||
|
||||
export const PREFIX_QUERY_ARG_QUERY_TYPE = 'query_type_';
|
||||
export const PREFIX_QUERY_ARG_FILTER_TYPE = 'filter_';
|
||||
|
||||
export function getUrlParameter( name: string ) {
|
||||
if ( ! window ) {
|
||||
return null;
|
||||
}
|
||||
return getQueryArg( window.location.href, name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the URL and reload the page if filtering for PHP templates.
|
||||
*
|
||||
* @param {string} newUrl New URL to be set.
|
||||
*/
|
||||
export function changeUrl( newUrl: string ) {
|
||||
if ( filteringForPhpTemplate ) {
|
||||
/**
|
||||
* We want to remove page number from URL whenever filters are changed.
|
||||
* This will move the user to the first page of results.
|
||||
*
|
||||
* There are following page number formats:
|
||||
* 1. query-{number}-page={number} (ex. query-1-page=2)
|
||||
* - ref: https://github.com/WordPress/gutenberg/blob/5693a62214b6c76d3dc5f3f69d8aad187748af79/packages/block-library/src/query-pagination-numbers/index.php#L18
|
||||
* 2. query-page={number} (ex. query-page=2)
|
||||
* - ref: same as above
|
||||
* 3. page/{number} (ex. page/2) (Default WordPress pagination format)
|
||||
*/
|
||||
newUrl = newUrl.replace(
|
||||
/(?:query-(?:\d+-)?page=(\d+))|(?:page\/(\d+))/g,
|
||||
''
|
||||
);
|
||||
|
||||
/**
|
||||
* If the URL ends with '?', we remove the trailing '?' from the URL.
|
||||
* The trailing '?' in a URL is unnecessary and can cause the page to
|
||||
* reload, which can negatively affect performance. By removing the '?',
|
||||
* we prevent this unnecessary reload. This is safe to do even if there
|
||||
* are query parameters, as they will not be affected by the removal
|
||||
* of a trailing '?'.
|
||||
*/
|
||||
if ( newUrl.endsWith( '?' ) ) {
|
||||
newUrl = newUrl.slice( 0, -1 );
|
||||
}
|
||||
|
||||
window.location.href = newUrl;
|
||||
} else {
|
||||
window.history.replaceState( {}, '', newUrl );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the query params through buildQueryString to normalise the params.
|
||||
*
|
||||
* @param {string} url URL to encode the search param from.
|
||||
*/
|
||||
export const normalizeQueryParams = ( url: string ) => {
|
||||
const queryArgs = getQueryArgs( url );
|
||||
return addQueryArgs( url, queryArgs );
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Recursively searches through an array of `BlockInstance` objects and their nested `innerBlocks` arrays to find a block that matches a given condition.
|
||||
*
|
||||
* @param { { blocks: BlockInstance[], findCondition: Function } } parameters Parameters containing an array of `BlockInstance` objects to search through and a function that takes a `BlockInstance` object as its argument and returns a boolean indicating whether the block matches the desired condition.
|
||||
* @return If a matching block is found, the function returns the `BlockInstance` object. If no matching block is found, the function returns `undefined`.
|
||||
*/
|
||||
export const findBlock = ( {
|
||||
blocks,
|
||||
findCondition,
|
||||
}: {
|
||||
blocks: BlockInstance[];
|
||||
findCondition: ( block: BlockInstance ) => boolean;
|
||||
} ): BlockInstance | undefined => {
|
||||
for ( const block of blocks ) {
|
||||
if ( findCondition( block ) ) {
|
||||
return block;
|
||||
}
|
||||
if ( block.innerBlocks ) {
|
||||
const foundChildBlock = findBlock( {
|
||||
blocks: block.innerBlocks,
|
||||
findCondition,
|
||||
} );
|
||||
if ( foundChildBlock ) {
|
||||
return foundChildBlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from './array-operations';
|
||||
export * from './attributes-query';
|
||||
export * from './attributes';
|
||||
export * from './filters';
|
||||
export * from './notices';
|
||||
export * from './object-operations';
|
||||
export * from './products';
|
||||
export * from './shared-attributes';
|
||||
export * from './sanitize-html';
|
||||
export * from './is-site-editor-page';
|
||||
export * from './is-widget-editor-page';
|
||||
export * from './trim-words';
|
||||
export * from './find-block';
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { isObject } from '../types/type-guards';
|
||||
|
||||
export const isSiteEditorPage = ( store: unknown ): boolean => {
|
||||
if ( isObject( store ) ) {
|
||||
const editedPostType = (
|
||||
store as {
|
||||
getEditedPostType: () => string;
|
||||
}
|
||||
).getEditedPostType();
|
||||
|
||||
return (
|
||||
editedPostType === 'wp_template' ||
|
||||
editedPostType === 'wp_template_part'
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { isObject } from '../types/type-guards';
|
||||
|
||||
export const isWidgetEditorPage = ( store: unknown ): boolean => {
|
||||
if ( isObject( store ) ) {
|
||||
const widgetAreas = (
|
||||
store as {
|
||||
getWidgetAreas: () => string;
|
||||
}
|
||||
).getWidgetAreas();
|
||||
|
||||
return Array.isArray( widgetAreas ) && widgetAreas.length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { dispatch, select } from '@wordpress/data';
|
||||
import type { Notice } from '@wordpress/notices';
|
||||
|
||||
export const hasNoticesOfType = (
|
||||
type: 'default' | 'snackbar',
|
||||
context?: string | undefined
|
||||
): boolean => {
|
||||
const notices: Notice[] = select( 'core/notices' ).getNotices( context );
|
||||
return notices.some( ( notice: Notice ) => notice.type === type );
|
||||
};
|
||||
|
||||
// Note, if context is blank, the default context is used.
|
||||
export const removeNoticesByStatus = (
|
||||
status: string,
|
||||
context?: string | undefined
|
||||
): void => {
|
||||
const notices = select( 'core/notices' ).getNotices( context );
|
||||
const { removeNotice } = dispatch( 'core/notices' );
|
||||
const noticesOfType = notices.filter(
|
||||
( notice ) => notice.status === status
|
||||
);
|
||||
noticesOfType.forEach( ( notice ) => removeNotice( notice.id, context ) );
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Returns an object without a key.
|
||||
*/
|
||||
export function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
|
||||
const { [ key ]: omit, ...rest } = obj;
|
||||
|
||||
return rest;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
|
||||
import type { ProductResponseItem } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Converts a Product object into a shape compatible with the `SearchListControl`
|
||||
*/
|
||||
export const convertProductResponseItemToSearchItem = (
|
||||
product: ProductResponseItem
|
||||
): SearchListItem< ProductResponseItem > => {
|
||||
const { id, name, parent } = product;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
breadcrumbs: [],
|
||||
children: [],
|
||||
details: product,
|
||||
value: product.slug,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the src of the first image attached to a product (the featured image).
|
||||
*/
|
||||
export function getImageSrcFromProduct( product: ProductResponseItem ) {
|
||||
if ( ! product || ! product.images || ! product.images.length ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return product.images[ 0 ].src || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the first image attached to a product (the featured image).
|
||||
*/
|
||||
export function getImageIdFromProduct( product: ProductResponseItem ) {
|
||||
if ( ! product || ! product.images || ! product.images.length ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return product.images[ 0 ].id || 0;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ];
|
||||
const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ];
|
||||
|
||||
export const sanitizeHTML = (
|
||||
html: string,
|
||||
config?: { tags?: typeof ALLOWED_TAGS; attr?: typeof ALLOWED_ATTR }
|
||||
) => {
|
||||
const tagsValue = config?.tags || ALLOWED_TAGS;
|
||||
const attrValue = config?.attr || ALLOWED_ATTR;
|
||||
|
||||
return DOMPurify.sanitize( html, {
|
||||
ALLOWED_TAGS: tagsValue,
|
||||
ALLOWED_ATTR: attrValue,
|
||||
} );
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
export const sharedAttributeBlockTypes = [
|
||||
'woocommerce/product-best-sellers',
|
||||
'woocommerce/product-category',
|
||||
'woocommerce/product-new',
|
||||
'woocommerce/product-on-sale',
|
||||
'woocommerce/product-top-rated',
|
||||
];
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Number of columns.
|
||||
*/
|
||||
columns: {
|
||||
type: 'number',
|
||||
default: getSetting( 'defaultColumns', 3 ),
|
||||
},
|
||||
|
||||
/**
|
||||
* Number of rows.
|
||||
*/
|
||||
rows: {
|
||||
type: 'number',
|
||||
default: getSetting( 'defaultRows', 3 ),
|
||||
},
|
||||
|
||||
/**
|
||||
* How to align cart buttons.
|
||||
*/
|
||||
alignButtons: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Product category, used to display only products in the given categories.
|
||||
*/
|
||||
categories: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
},
|
||||
|
||||
/**
|
||||
* Product category operator, used to restrict to products in all or any selected categories.
|
||||
*/
|
||||
catOperator: {
|
||||
type: 'string',
|
||||
default: 'any',
|
||||
},
|
||||
|
||||
/**
|
||||
* Content visibility setting
|
||||
*/
|
||||
contentVisibility: {
|
||||
type: 'object',
|
||||
default: {
|
||||
image: true,
|
||||
title: true,
|
||||
price: true,
|
||||
rating: true,
|
||||
button: true,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Are we previewing?
|
||||
*/
|
||||
isPreview: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to display in stock, out of stock or backorder products.
|
||||
*/
|
||||
stockStatus: {
|
||||
type: 'array',
|
||||
default: Object.keys( getSetting( 'stockStatusOptions', [] ) ),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { normalizeQueryParams } from '../filters';
|
||||
|
||||
describe( 'normalizeQueryParams', () => {
|
||||
test( 'does not change url if there is no query params', () => {
|
||||
const input = 'https://example.com';
|
||||
const expected = 'https://example.com';
|
||||
|
||||
expect( normalizeQueryParams( input ) ).toBe( expected );
|
||||
} );
|
||||
|
||||
test( 'does not change search term if there is no special character', () => {
|
||||
const input = 'https://example.com?foo=bar&s=asdf1234&baz=qux';
|
||||
const expected = 'https://example.com?foo=bar&s=asdf1234&baz=qux';
|
||||
|
||||
expect( normalizeQueryParams( input ) ).toBe( expected );
|
||||
} );
|
||||
|
||||
test( 'decodes single quote characters', () => {
|
||||
const input = 'https://example.com?foo=bar%27&s=asd%27f1234&baz=qux%27';
|
||||
const expected = "https://example.com?foo=bar'&s=asd'f1234&baz=qux'";
|
||||
|
||||
expect( normalizeQueryParams( input ) ).toBe( expected );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select, dispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { hasNoticesOfType, removeNoticesByStatus } from '../notices';
|
||||
|
||||
jest.mock( '@wordpress/data' );
|
||||
|
||||
describe( 'Notice utils', () => {
|
||||
beforeEach( () => {
|
||||
jest.resetAllMocks();
|
||||
} );
|
||||
describe( 'hasNoticesOfType', () => {
|
||||
it( 'Correctly returns if there are notices of a given type in the core data store', () => {
|
||||
select.mockReturnValue( {
|
||||
getNotices: jest.fn().mockReturnValue( [
|
||||
{
|
||||
id: 'coupon-form',
|
||||
status: 'error',
|
||||
content:
|
||||
'Coupon cannot be removed because it is not already applied to the cart.',
|
||||
spokenMessage:
|
||||
'Coupon cannot be removed because it is not already applied to the cart.',
|
||||
isDismissible: true,
|
||||
actions: [],
|
||||
type: 'default',
|
||||
icon: null,
|
||||
explicitDismiss: false,
|
||||
},
|
||||
] ),
|
||||
} );
|
||||
const hasSnackbarNotices = hasNoticesOfType(
|
||||
'snackbar',
|
||||
'wc/cart'
|
||||
);
|
||||
const hasDefaultNotices = hasNoticesOfType( 'default', 'wc/cart' );
|
||||
expect( hasDefaultNotices ).toBe( true );
|
||||
expect( hasSnackbarNotices ).toBe( false );
|
||||
} );
|
||||
|
||||
it( 'Handles notices being empty', () => {
|
||||
select.mockReturnValue( {
|
||||
getNotices: jest.fn().mockReturnValue( [] ),
|
||||
} );
|
||||
const hasDefaultNotices = hasNoticesOfType( 'default', 'wc/cart' );
|
||||
expect( hasDefaultNotices ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
describe( 'removeNoticesByStatus', () => {
|
||||
it( 'Correctly removes notices of a given status', () => {
|
||||
select.mockReturnValue( {
|
||||
getNotices: jest.fn().mockReturnValue( [
|
||||
{
|
||||
id: 'coupon-form',
|
||||
status: 'error',
|
||||
content:
|
||||
'Coupon cannot be removed because it is not already applied to the cart.',
|
||||
spokenMessage:
|
||||
'Coupon cannot be removed because it is not already applied to the cart.',
|
||||
isDismissible: true,
|
||||
actions: [],
|
||||
type: 'default',
|
||||
icon: null,
|
||||
explicitDismiss: false,
|
||||
},
|
||||
{
|
||||
id: 'address-form',
|
||||
status: 'error',
|
||||
content: 'Address invalid',
|
||||
spokenMessage: 'Address invalid',
|
||||
isDismissible: true,
|
||||
actions: [],
|
||||
type: 'default',
|
||||
icon: null,
|
||||
explicitDismiss: false,
|
||||
},
|
||||
{
|
||||
id: 'some-warning',
|
||||
status: 'warning',
|
||||
content: 'Warning notice.',
|
||||
spokenMessage: 'Warning notice.',
|
||||
isDismissible: true,
|
||||
actions: [],
|
||||
type: 'default',
|
||||
icon: null,
|
||||
explicitDismiss: false,
|
||||
},
|
||||
] ),
|
||||
} );
|
||||
dispatch.mockReturnValue( {
|
||||
removeNotice: jest.fn(),
|
||||
} );
|
||||
removeNoticesByStatus( 'error' );
|
||||
expect( dispatch().removeNotice ).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'coupon-form',
|
||||
undefined
|
||||
);
|
||||
expect( dispatch().removeNotice ).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'address-form',
|
||||
undefined
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'Handles notices being empty', () => {
|
||||
select.mockReturnValue( {
|
||||
getNotices: jest.fn().mockReturnValue( [] ),
|
||||
} );
|
||||
|
||||
dispatch.mockReturnValue( {
|
||||
removeNotice: jest.fn(),
|
||||
} );
|
||||
removeNoticesByStatus( 'empty' );
|
||||
expect( dispatch().removeNotice ).not.toBeCalled();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getImageSrcFromProduct, getImageIdFromProduct } from '../products';
|
||||
|
||||
describe( 'getImageSrcFromProduct', () => {
|
||||
test( 'returns first image src', () => {
|
||||
const imageSrc = getImageSrcFromProduct( {
|
||||
images: [ { src: 'foo.jpg' } ],
|
||||
} );
|
||||
|
||||
expect( imageSrc ).toBe( 'foo.jpg' );
|
||||
} );
|
||||
|
||||
test( 'returns empty string if no product was provided', () => {
|
||||
const imageSrc = getImageSrcFromProduct();
|
||||
|
||||
expect( imageSrc ).toBe( '' );
|
||||
} );
|
||||
|
||||
test( 'returns empty string if product is empty', () => {
|
||||
const imageSrc = getImageSrcFromProduct( {} );
|
||||
|
||||
expect( imageSrc ).toBe( '' );
|
||||
} );
|
||||
|
||||
test( 'returns empty string if product has no images', () => {
|
||||
const imageSrc = getImageSrcFromProduct( { images: null } );
|
||||
|
||||
expect( imageSrc ).toBe( '' );
|
||||
} );
|
||||
|
||||
test( 'returns empty string if product has 0 images', () => {
|
||||
const imageSrc = getImageSrcFromProduct( { images: [] } );
|
||||
|
||||
expect( imageSrc ).toBe( '' );
|
||||
} );
|
||||
|
||||
test( 'returns empty string if product image has no src attribute', () => {
|
||||
const imageSrc = getImageSrcFromProduct( { images: [ {} ] } );
|
||||
|
||||
expect( imageSrc ).toBe( '' );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getImageIdFromProduct', () => {
|
||||
test( 'returns first image id', () => {
|
||||
const imageUrl = getImageIdFromProduct( {
|
||||
images: [ { id: 123 } ],
|
||||
} );
|
||||
|
||||
expect( imageUrl ).toBe( 123 );
|
||||
} );
|
||||
|
||||
test( 'returns 0 if no product was provided', () => {
|
||||
const imageUrl = getImageIdFromProduct();
|
||||
|
||||
expect( imageUrl ).toBe( 0 );
|
||||
} );
|
||||
|
||||
test( 'returns 0 if product is empty', () => {
|
||||
const imageUrl = getImageIdFromProduct( {} );
|
||||
|
||||
expect( imageUrl ).toBe( 0 );
|
||||
} );
|
||||
|
||||
test( 'returns 0 if product has no images', () => {
|
||||
const imageUrl = getImageIdFromProduct( { images: null } );
|
||||
|
||||
expect( imageUrl ).toBe( 0 );
|
||||
} );
|
||||
|
||||
test( 'returns 0 if product has 0 images', () => {
|
||||
const imageUrl = getImageIdFromProduct( { images: [] } );
|
||||
|
||||
expect( imageUrl ).toBe( 0 );
|
||||
} );
|
||||
|
||||
test( 'returns 0 if product image has no src attribute', () => {
|
||||
const imageUrl = getImageIdFromProduct( { images: [ {} ] } );
|
||||
|
||||
expect( imageUrl ).toBe( 0 );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
appendMoreText,
|
||||
removeTags,
|
||||
trimCharacters,
|
||||
trimWords,
|
||||
} from '@woocommerce/utils';
|
||||
|
||||
describe( 'trim-words', () => {
|
||||
describe( 'removeTags', () => {
|
||||
it( 'Removes HTML tags from a string', () => {
|
||||
const string = '<div><a href="/index.php">trim-words.ts</a></div>';
|
||||
const trimmedString = removeTags( string );
|
||||
expect( trimmedString ).toEqual( 'trim-words.ts' );
|
||||
} );
|
||||
} );
|
||||
describe( 'appendMoreText', () => {
|
||||
it( 'Removes trailing punctuation and appends some characters to a string', () => {
|
||||
const string = 'trim-words.ts,';
|
||||
const appendedString = appendMoreText( string, '...' );
|
||||
expect( appendedString ).toEqual( 'trim-words.ts...' );
|
||||
} );
|
||||
} );
|
||||
describe( 'trimWords', () => {
|
||||
const testContent =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.';
|
||||
it( 'Limits words in string and returns trimmed version', () => {
|
||||
const trimmedString = trimWords( testContent, 3 );
|
||||
expect( trimmedString ).toBe(
|
||||
'<p>Lorem ipsum dolor…</p>\n'
|
||||
);
|
||||
} );
|
||||
it( 'Limits words in string and returns trimmed version with custom moreText', () => {
|
||||
const trimmedString = trimWords( testContent, 4, '... read more.' );
|
||||
expect( trimmedString ).toEqual(
|
||||
'<p>Lorem ipsum dolor sit... read more.</p>\n'
|
||||
);
|
||||
} );
|
||||
it( 'Limits words in string and returns trimmed version without autop', () => {
|
||||
const trimmedString = trimWords(
|
||||
testContent,
|
||||
3,
|
||||
'…',
|
||||
false
|
||||
);
|
||||
expect( trimmedString ).toEqual( 'Lorem ipsum dolor…' );
|
||||
} );
|
||||
it( 'does not append anything if the text is shorter than the trim limit', () => {
|
||||
const trimmedString = trimWords( testContent, 100 );
|
||||
expect( trimmedString ).toEqual( '<p>' + testContent + '</p>\n' );
|
||||
} );
|
||||
} );
|
||||
describe( 'trimCharacters', () => {
|
||||
const testContent = 'Lorem ipsum dolor sit amet.';
|
||||
|
||||
it( 'Limits characters in string and returns trimmed version including spaces', () => {
|
||||
const result = trimCharacters( testContent, 10 );
|
||||
expect( result ).toEqual( '<p>Lorem ipsu…</p>\n' );
|
||||
} );
|
||||
it( 'Limits characters in string and returns trimmed version excluding spaces', () => {
|
||||
const result = trimCharacters( testContent, 10, false );
|
||||
expect( result ).toEqual( '<p>Lorem ipsum…</p>\n' );
|
||||
} );
|
||||
it( 'Limits characters in string and returns trimmed version with custom moreText', () => {
|
||||
const result = trimCharacters(
|
||||
testContent,
|
||||
10,
|
||||
false,
|
||||
'... read more.'
|
||||
);
|
||||
expect( result ).toEqual( '<p>Lorem ipsum... read more.</p>\n' );
|
||||
} );
|
||||
it( 'Limits characters in string and returns trimmed version without autop', () => {
|
||||
const result = trimCharacters(
|
||||
testContent,
|
||||
10,
|
||||
false,
|
||||
'... read more.',
|
||||
false
|
||||
);
|
||||
expect( result ).toEqual( 'Lorem ipsum... read more.' );
|
||||
} );
|
||||
|
||||
it( 'does not append anything if the text is shorter than the trim limit', () => {
|
||||
const trimmedString = trimCharacters( testContent, 1000 );
|
||||
expect( trimmedString ).toEqual( '<p>' + testContent + '</p>\n' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { autop } from '@wordpress/autop';
|
||||
|
||||
/**
|
||||
* Remove HTML tags from a string.
|
||||
*
|
||||
* @param {string} htmlString String to remove tags from.
|
||||
* @return {string} Plain text string.
|
||||
*/
|
||||
export const removeTags = ( htmlString: string ) => {
|
||||
const tagsRegExp = /<\/?[a-z][^>]*?>/gi;
|
||||
return htmlString.replace( tagsRegExp, '' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove trailing punctuation and append some characters to a string.
|
||||
*
|
||||
* @param {string} text Text to append to.
|
||||
* @param {string} moreText Text to append.
|
||||
* @return {string} String with appended characters.
|
||||
*/
|
||||
export const appendMoreText = ( text: string, moreText: string ) => {
|
||||
return text.replace( /[\s|\.\,]+$/i, '' ) + moreText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Limit words in string and returned trimmed version.
|
||||
*
|
||||
* @param {string} text Text to trim.
|
||||
* @param {number} maxLength Number of countType to limit to.
|
||||
* @param {string} moreText Appended to the trimmed string.
|
||||
* @param {string} useAutop Whether to format with autop before returning.
|
||||
* @return {string} Trimmed string.
|
||||
*/
|
||||
export const trimWords = (
|
||||
text: string,
|
||||
maxLength: number,
|
||||
moreText = '…',
|
||||
useAutop = true
|
||||
) => {
|
||||
const textToTrim = removeTags( text );
|
||||
const trimmedText = textToTrim
|
||||
.split( ' ' )
|
||||
.splice( 0, maxLength )
|
||||
.join( ' ' );
|
||||
|
||||
if ( trimmedText === textToTrim ) {
|
||||
return useAutop ? autop( textToTrim ) : textToTrim;
|
||||
}
|
||||
|
||||
if ( ! useAutop ) {
|
||||
return appendMoreText( trimmedText, moreText );
|
||||
}
|
||||
|
||||
return autop( appendMoreText( trimmedText, moreText ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Limit characters in string and returned trimmed version.
|
||||
*
|
||||
* @param {string} text Text to trim.
|
||||
* @param {number} maxLength Number of countType to limit to.
|
||||
* @param {boolean} includeSpaces Should spaces be included in the count.
|
||||
* @param {string} moreText Appended to the trimmed string.
|
||||
* @param {string} useAutop Whether to format with autop before returning.
|
||||
* @return {string} Trimmed string.
|
||||
*/
|
||||
export const trimCharacters = (
|
||||
text: string,
|
||||
maxLength: number,
|
||||
includeSpaces = true,
|
||||
moreText = '…',
|
||||
useAutop = true
|
||||
) => {
|
||||
const textToTrim = removeTags( text );
|
||||
const trimmedText = textToTrim.slice( 0, maxLength );
|
||||
|
||||
if ( trimmedText === textToTrim ) {
|
||||
return useAutop ? autop( textToTrim ) : textToTrim;
|
||||
}
|
||||
|
||||
if ( includeSpaces ) {
|
||||
return autop( appendMoreText( trimmedText, moreText ) );
|
||||
}
|
||||
|
||||
const matchSpaces = trimmedText.match( /([\s]+)/g );
|
||||
const spaceCount = matchSpaces ? matchSpaces.length : 0;
|
||||
const trimmedTextExcludingSpaces = textToTrim.slice(
|
||||
0,
|
||||
maxLength + spaceCount
|
||||
);
|
||||
|
||||
if ( ! useAutop ) {
|
||||
return appendMoreText( trimmedTextExcludingSpaces, moreText );
|
||||
}
|
||||
|
||||
return autop( appendMoreText( trimmedTextExcludingSpaces, moreText ) );
|
||||
};
|
||||
Reference in New Issue
Block a user