Implement tabular data filtering (https://github.com/woocommerce/woocommerce-admin/pull/1381)
* Move labels loading logic into a lib * Move lib/labels into lib/async-requests * Implement tabular data filtering * Allow searching for string in report tables * Add table filtering to customers table * Get ids from searched string to populate the table * Fix autocompleter keyboard interactions * Improve props naming * Cleanup report customers data store * Prevent an edge case issue that might not update the selectedOptions when directily modifying the URL * Fix wrong selected autocompleter option * Add missing translation domain * Move searchItemsByString to wc-api/items/utils.js * Avoid autocompleter results appearing when there was no search string * Alphabetically order 'allowFreeTextSearch' prop * Reset selected table rows when directly modifying the URL * Simplify props destructuring * Undo customers data store change * Simplify isProductDetailsView expression * Improve order * Filter tax code parts before modifying them
This commit is contained in:
parent
1069034ca2
commit
d8ed3b6614
|
@ -153,6 +153,7 @@ class CategoriesReportTable extends Component {
|
|||
getSummary={ this.getSummary }
|
||||
itemIdField="category_id"
|
||||
query={ query }
|
||||
searchBy="categories"
|
||||
labels={ labels }
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'items_sold',
|
||||
|
@ -168,14 +169,14 @@ class CategoriesReportTable extends Component {
|
|||
|
||||
export default compose(
|
||||
withSelect( select => {
|
||||
const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
|
||||
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
|
||||
const tableQuery = {
|
||||
per_page: -1,
|
||||
};
|
||||
|
||||
const categories = getCategories( tableQuery );
|
||||
const isError = Boolean( getCategoriesError( tableQuery ) );
|
||||
const isRequesting = isGetCategoriesRequesting( tableQuery );
|
||||
const categories = getItems( 'categories', tableQuery );
|
||||
const isError = Boolean( getItemsError( 'categories', tableQuery ) );
|
||||
const isRequesting = isGetItemsRequesting( 'categories', tableQuery );
|
||||
|
||||
return { categories, isError, isRequesting };
|
||||
} )
|
||||
|
|
|
@ -169,6 +169,7 @@ export default class CouponsReportTable extends Component {
|
|||
getSummary={ this.getSummary }
|
||||
itemIdField="coupon_id"
|
||||
query={ query }
|
||||
searchBy="coupons"
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'orders_count',
|
||||
order: query.order || 'desc',
|
||||
|
|
|
@ -227,7 +227,6 @@ export default class CustomersReportTable extends Component {
|
|||
query={ query }
|
||||
labels={ { placeholder: __( 'Search by customer name', 'wc-admin' ) } }
|
||||
searchBy="customers"
|
||||
searchParam="name_includes"
|
||||
title={ __( 'Customers', 'wc-admin' ) }
|
||||
columnPrefsKey="customers_report_columns"
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
import { find } from 'lodash';
|
||||
|
||||
|
@ -12,6 +13,7 @@ import { find } from 'lodash';
|
|||
* WooCommerce dependencies
|
||||
*/
|
||||
import { useFilters } from '@woocommerce/components';
|
||||
import { getQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -27,6 +29,8 @@ import TaxesReport from './taxes';
|
|||
import DownloadsReport from './downloads';
|
||||
import StockReport from './stock';
|
||||
import CustomersReport from './customers';
|
||||
import { searchItemsByString } from 'wc-api/items/utils';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
const REPORTS_FILTER = 'woocommerce-reports-list';
|
||||
|
||||
|
@ -131,4 +135,27 @@ Report.propTypes = {
|
|||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default useFilters( REPORTS_FILTER )( Report );
|
||||
export default compose(
|
||||
useFilters( REPORTS_FILTER ),
|
||||
withSelect( ( select, props ) => {
|
||||
const { search } = getQuery();
|
||||
|
||||
if ( ! search ) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { report } = props.params;
|
||||
const items = searchItemsByString( select, report, search );
|
||||
const ids = Object.keys( items );
|
||||
if ( ! ids.length ) {
|
||||
return {}; // @TODO if no results were found, we should avoid making a server request.
|
||||
}
|
||||
|
||||
return {
|
||||
query: {
|
||||
...props.query,
|
||||
[ report ]: ids.join( ',' ),
|
||||
},
|
||||
};
|
||||
} )
|
||||
)( Report );
|
||||
|
|
|
@ -24,7 +24,7 @@ import VariationsReportTable from './table-variations';
|
|||
export default class ProductsReport extends Component {
|
||||
render() {
|
||||
const { path, query } = this.props;
|
||||
const isProductDetailsView = query.products && 1 === query.products.split( ',' ).length;
|
||||
const isProductDetailsView = query.filter === 'single_product';
|
||||
|
||||
const itemsLabel = isProductDetailsView
|
||||
? __( '%s variations', 'wc-admin' )
|
||||
|
|
|
@ -182,6 +182,7 @@ export default class VariationsReportTable extends Component {
|
|||
labels={ labels }
|
||||
query={ query }
|
||||
getSummary={ this.getSummary }
|
||||
searchBy="variations"
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'items_sold',
|
||||
order: query.order || 'desc',
|
||||
|
|
|
@ -245,6 +245,7 @@ class ProductsReportTable extends Component {
|
|||
itemIdField="product_id"
|
||||
labels={ labels }
|
||||
query={ query }
|
||||
searchBy="products"
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'items_sold',
|
||||
order: query.order || 'desc',
|
||||
|
@ -259,14 +260,14 @@ class ProductsReportTable extends Component {
|
|||
|
||||
export default compose(
|
||||
withSelect( select => {
|
||||
const { getCategories, getCategoriesError, isGetCategoriesRequesting } = select( 'wc-api' );
|
||||
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
|
||||
const tableQuery = {
|
||||
per_page: -1,
|
||||
};
|
||||
|
||||
const categories = getCategories( tableQuery );
|
||||
const isError = Boolean( getCategoriesError( tableQuery ) );
|
||||
const isRequesting = isGetCategoriesRequesting( tableQuery );
|
||||
const categories = getItems( 'categories', tableQuery );
|
||||
const isError = Boolean( getItemsError( 'categories', tableQuery ) );
|
||||
const isRequesting = isGetItemsRequesting( 'categories', tableQuery );
|
||||
|
||||
return { categories, isError, isRequesting };
|
||||
} )
|
||||
|
|
|
@ -150,6 +150,7 @@ export default class TaxesReportTable extends Component {
|
|||
getSummary={ this.getSummary }
|
||||
itemIdField="tax_rate_id"
|
||||
query={ query }
|
||||
searchBy="taxes"
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'tax_rate_id',
|
||||
} }
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { stringifyQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { isResourcePrefix, getResourceIdentifier, getResourceName } from '../utils';
|
||||
import { NAMESPACE } from '../constants';
|
||||
|
||||
function read( resourceNames, fetch = apiFetch ) {
|
||||
const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'category-query' ) );
|
||||
|
||||
return filteredNames.map( async resourceName => {
|
||||
const query = getResourceIdentifier( resourceName );
|
||||
const url = NAMESPACE + `/products/categories${ stringifyQuery( query ) }`;
|
||||
|
||||
try {
|
||||
const categories = await fetch( {
|
||||
path: url,
|
||||
} );
|
||||
|
||||
const ids = categories.map( category => category.id );
|
||||
const categoryResources = categories.reduce( ( resources, category ) => {
|
||||
resources[ getResourceName( 'category', category.id ) ] = { data: category };
|
||||
return resources;
|
||||
}, {} );
|
||||
|
||||
return {
|
||||
[ resourceName ]: {
|
||||
data: ids,
|
||||
totalCount: ids.length,
|
||||
},
|
||||
...categoryResources,
|
||||
};
|
||||
} catch ( error ) {
|
||||
return { [ resourceName ]: { error } };
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
export default {
|
||||
read,
|
||||
};
|
|
@ -1,56 +0,0 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourceName } from '../utils';
|
||||
import { DEFAULT_REQUIREMENT } from '../constants';
|
||||
|
||||
const getCategories = ( getResource, requireResource ) => (
|
||||
query = {},
|
||||
requirement = DEFAULT_REQUIREMENT
|
||||
) => {
|
||||
const resourceName = getResourceName( 'category-query', query );
|
||||
const ids = requireResource( requirement, resourceName ).data || [];
|
||||
const categories = ids.reduce(
|
||||
( acc, id ) => ( {
|
||||
...acc,
|
||||
[ id ]: getResource( getResourceName( 'category', id ) ).data || {},
|
||||
} ),
|
||||
{}
|
||||
);
|
||||
return categories;
|
||||
};
|
||||
|
||||
const getCategoriesTotalCount = getResource => ( query = {} ) => {
|
||||
const resourceName = getResourceName( 'category-query', query );
|
||||
return getResource( resourceName ).totalCount || 0;
|
||||
};
|
||||
|
||||
const getCategoriesError = getResource => ( query = {} ) => {
|
||||
const resourceName = getResourceName( 'category-query', query );
|
||||
return getResource( resourceName ).error;
|
||||
};
|
||||
|
||||
const isGetCategoriesRequesting = getResource => ( query = {} ) => {
|
||||
const resourceName = getResourceName( 'category-query', query );
|
||||
const { lastRequested, lastReceived } = getResource( resourceName );
|
||||
|
||||
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return lastRequested > lastReceived;
|
||||
};
|
||||
|
||||
export default {
|
||||
getCategories,
|
||||
getCategoriesError,
|
||||
getCategoriesTotalCount,
|
||||
isGetCategoriesRequesting,
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { stringifyQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourceIdentifier, getResourcePrefix, getResourceName } from '../utils';
|
||||
import { NAMESPACE } from '../constants';
|
||||
|
||||
const typeEndpointMap = {
|
||||
'items-query-categories': 'products/categories',
|
||||
'items-query-customers': 'customers',
|
||||
'items-query-coupons': 'coupons',
|
||||
'items-query-products': 'products',
|
||||
'items-query-taxes': 'taxes',
|
||||
};
|
||||
|
||||
function read( resourceNames, fetch = apiFetch ) {
|
||||
const filteredNames = resourceNames.filter( name => {
|
||||
const prefix = getResourcePrefix( name );
|
||||
return Boolean( typeEndpointMap[ prefix ] );
|
||||
} );
|
||||
|
||||
return filteredNames.map( async resourceName => {
|
||||
const prefix = getResourcePrefix( resourceName );
|
||||
const endpoint = typeEndpointMap[ prefix ];
|
||||
const query = getResourceIdentifier( resourceName );
|
||||
const url = NAMESPACE + `/${ endpoint }${ stringifyQuery( query ) }`;
|
||||
|
||||
try {
|
||||
const items = await fetch( {
|
||||
path: url,
|
||||
} );
|
||||
|
||||
const ids = items.map( item => item.id );
|
||||
const itemResources = items.reduce( ( resources, item ) => {
|
||||
resources[ getResourceName( `${ prefix }-item`, item.id ) ] = { data: item };
|
||||
return resources;
|
||||
}, {} );
|
||||
|
||||
return {
|
||||
[ resourceName ]: {
|
||||
data: ids,
|
||||
totalCount: ids.length,
|
||||
},
|
||||
...itemResources,
|
||||
};
|
||||
} catch ( error ) {
|
||||
return { [ resourceName ]: { error } };
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
export default {
|
||||
read,
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getResourceName } from '../utils';
|
||||
import { DEFAULT_REQUIREMENT } from '../constants';
|
||||
|
||||
const getItems = ( getResource, requireResource ) => (
|
||||
type,
|
||||
query = {},
|
||||
requirement = DEFAULT_REQUIREMENT
|
||||
) => {
|
||||
const resourceName = getResourceName( `items-query-${ type }`, query );
|
||||
const ids = requireResource( requirement, resourceName ).data || [];
|
||||
const items = ids.reduce(
|
||||
( acc, id ) => ( {
|
||||
...acc,
|
||||
[ id ]: getResource( getResourceName( `items-query-${ type }-item`, id ) ).data || {},
|
||||
} ),
|
||||
{}
|
||||
);
|
||||
return items;
|
||||
};
|
||||
|
||||
const getItemsTotalCount = getResource => ( type, query = {} ) => {
|
||||
const resourceName = getResourceName( `items-query-${ type }`, query );
|
||||
return getResource( resourceName ).totalCount || 0;
|
||||
};
|
||||
|
||||
const getItemsError = getResource => ( type, query = {} ) => {
|
||||
const resourceName = getResourceName( `items-query-${ type }`, query );
|
||||
return getResource( resourceName ).error;
|
||||
};
|
||||
|
||||
const isGetItemsRequesting = getResource => ( type, query = {} ) => {
|
||||
const resourceName = getResourceName( `items-query-${ type }`, query );
|
||||
const { lastRequested, lastReceived } = getResource( resourceName );
|
||||
|
||||
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return lastRequested > lastReceived;
|
||||
};
|
||||
|
||||
export default {
|
||||
getItems,
|
||||
getItemsError,
|
||||
getItemsTotalCount,
|
||||
isGetItemsRequesting,
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns items based on a search query.
|
||||
*
|
||||
* @param {Object} select Instance of @wordpress/select
|
||||
* @param {String} endpoint Report API Endpoint
|
||||
* @param {String} search Search strings separated by commas.
|
||||
* @return {Object} Object Object containing the matching items.
|
||||
*/
|
||||
export function searchItemsByString( select, endpoint, search ) {
|
||||
const { getItems } = select( 'wc-api' );
|
||||
const searchWords = search.split( ',' );
|
||||
|
||||
const items = searchWords.reduce( ( acc, searchWord ) => {
|
||||
return {
|
||||
...acc,
|
||||
...getItems( endpoint, {
|
||||
search: searchWord,
|
||||
per_page: 10,
|
||||
} ),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return items;
|
||||
}
|
|
@ -37,6 +37,12 @@ const reportConfigs = {
|
|||
};
|
||||
|
||||
export function getFilterQuery( endpoint, query ) {
|
||||
if ( query.search ) {
|
||||
return {
|
||||
[ endpoint ]: query[ endpoint ],
|
||||
};
|
||||
}
|
||||
|
||||
if ( reportConfigs[ endpoint ] ) {
|
||||
const { filters = [], advancedFilters = {} } = reportConfigs[ endpoint ];
|
||||
return filters
|
||||
|
@ -335,7 +341,7 @@ export function getReportTableQuery( endpoint, urlQuery, query ) {
|
|||
*
|
||||
* @param {String} endpoint Report API Endpoint
|
||||
* @param {Object} urlQuery Query parameters in the url
|
||||
* @param {object} select Instance of @wordpress/select
|
||||
* @param {Object} select Instance of @wordpress/select
|
||||
* @param {Object} query Query parameters specific for that endpoint
|
||||
* @return {Object} Object Table data response
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import categories from './categories';
|
||||
import items from './items';
|
||||
import notes from './notes';
|
||||
import orders from './orders';
|
||||
import reportItems from './reports/items';
|
||||
|
@ -19,7 +19,7 @@ function createWcApiSpec() {
|
|||
...user.mutations,
|
||||
},
|
||||
selectors: {
|
||||
...categories.selectors,
|
||||
...items.selectors,
|
||||
...notes.selectors,
|
||||
...orders.selectors,
|
||||
...reportItems.selectors,
|
||||
|
@ -31,7 +31,7 @@ function createWcApiSpec() {
|
|||
operations: {
|
||||
read( resourceNames ) {
|
||||
return [
|
||||
...categories.operations.read( resourceNames ),
|
||||
...items.operations.read( resourceNames ),
|
||||
...notes.operations.read( resourceNames ),
|
||||
...orders.operations.read( resourceNames ),
|
||||
...reportItems.operations.read( resourceNames ),
|
||||
|
|
|
@ -204,7 +204,8 @@ export class Autocomplete extends Component {
|
|||
const expression = 'undefined' !== typeof completer.getSearchExpression
|
||||
? completer.getSearchExpression( escapeRegExp( query ) )
|
||||
: escapeRegExp( query );
|
||||
const search = new RegExp( expression, 'i' );
|
||||
// if there is no expression, match empty string
|
||||
const search = expression ? new RegExp( expression, 'i' ) : /^$/;
|
||||
// filter the options we already have
|
||||
const filteredOptions = filterOptions( search, this.state.options, selected );
|
||||
// update the state
|
||||
|
@ -215,26 +216,36 @@ export class Autocomplete extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
const { allowFreeText, completer } = this.props;
|
||||
const { getFreeTextOptions } = completer;
|
||||
const { filteredOptions, query } = this.state;
|
||||
|
||||
const additionalOptions = allowFreeText && getFreeTextOptions ? getFreeTextOptions( query ) : [];
|
||||
return additionalOptions.concat( filteredOptions );
|
||||
}
|
||||
|
||||
handleKeyDown( event ) {
|
||||
const { selectedIndex, filteredOptions } = this.state;
|
||||
if ( filteredOptions.length === 0 ) {
|
||||
const options = this.getOptions();
|
||||
const { selectedIndex } = this.state;
|
||||
if ( options.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
let nextSelectedIndex;
|
||||
switch ( event.keyCode ) {
|
||||
case UP:
|
||||
nextSelectedIndex = ( selectedIndex === 0 ? filteredOptions.length : selectedIndex ) - 1;
|
||||
nextSelectedIndex = ( selectedIndex === 0 ? options.length : selectedIndex ) - 1;
|
||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||
break;
|
||||
|
||||
case TAB:
|
||||
case DOWN:
|
||||
nextSelectedIndex = ( selectedIndex + 1 ) % filteredOptions.length;
|
||||
nextSelectedIndex = ( selectedIndex + 1 ) % options.length;
|
||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||
break;
|
||||
|
||||
case ENTER:
|
||||
this.select( filteredOptions[ selectedIndex ] );
|
||||
this.select( options[ selectedIndex ] );
|
||||
break;
|
||||
|
||||
case LEFT:
|
||||
|
@ -263,9 +274,13 @@ export class Autocomplete extends Component {
|
|||
this.node[ handler ]( 'keydown', this.handleKeyDown, true );
|
||||
}
|
||||
|
||||
isExpanded( props, state ) {
|
||||
return state.filteredOptions.length > 0 || ( props.completer.getFreeTextOptions && state.query );
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps, prevState ) {
|
||||
const isExpanded = this.state.filteredOptions.length > 0;
|
||||
const wasExpanded = prevState.filteredOptions.length > 0;
|
||||
const isExpanded = this.isExpanded( this.props, this.state );
|
||||
const wasExpanded = this.isExpanded( prevProps, prevState );
|
||||
if ( isExpanded && ! wasExpanded ) {
|
||||
this.toggleKeyEvents( true );
|
||||
} else if ( ! isExpanded && wasExpanded ) {
|
||||
|
@ -280,9 +295,10 @@ export class Autocomplete extends Component {
|
|||
|
||||
render() {
|
||||
const { children, instanceId, completer: { className = '' }, staticResults } = this.props;
|
||||
const { selectedIndex, filteredOptions, query } = this.state;
|
||||
const { key: selectedKey = '' } = filteredOptions[ selectedIndex ] || {};
|
||||
const isExpanded = filteredOptions.length > 0 && !! query;
|
||||
const { selectedIndex } = this.state;
|
||||
const isExpanded = this.isExpanded( this.props, this.state );
|
||||
const options = isExpanded ? this.getOptions() : [];
|
||||
const { key: selectedKey = '' } = options[ selectedIndex ] || {};
|
||||
const listBoxId = isExpanded ? `woocommerce-search__autocomplete-${ instanceId }` : null;
|
||||
const activeId = isExpanded
|
||||
? `woocommerce-search__autocomplete-${ instanceId }-${ selectedKey }`
|
||||
|
@ -290,12 +306,13 @@ export class Autocomplete extends Component {
|
|||
const resultsClasses = classnames( 'woocommerce-search__autocomplete-results', {
|
||||
'is-static-results': staticResults,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div ref={ this.bindNode } className="woocommerce-search__autocomplete">
|
||||
{ children( { isExpanded, listBoxId, activeId, onChange: this.search } ) }
|
||||
{ isExpanded && (
|
||||
<div id={ listBoxId } role="listbox" className={ resultsClasses }>
|
||||
{ filteredOptions.map( ( option, index ) => (
|
||||
{ options.map( ( option, index ) => (
|
||||
<Button
|
||||
key={ option.key }
|
||||
id={ `woocommerce-search__autocomplete-${ instanceId }-${ option.key }` }
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -33,12 +35,31 @@ export default {
|
|||
};
|
||||
payload = stringifyQuery( query );
|
||||
}
|
||||
return apiFetch( { path: `/wc/v3/products/categories${ payload }` } );
|
||||
return apiFetch( { path: `/wc/v4/products/categories${ payload }` } );
|
||||
},
|
||||
isDebounced: true,
|
||||
getOptionKeywords( cat ) {
|
||||
return [ cat.name ];
|
||||
},
|
||||
getFreeTextOptions( query ) {
|
||||
const label = (
|
||||
<span key="name" className="woocommerce-search__result-name">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __( 'All categories with titles that include {{query /}}', 'wc-admin' ),
|
||||
components: {
|
||||
query: <strong className="components-form-token-field__suggestion-match">{ query }</strong>,
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
);
|
||||
const titleOption = {
|
||||
key: 'title',
|
||||
label: label,
|
||||
value: { id: query, name: query },
|
||||
};
|
||||
|
||||
return [ titleOption ];
|
||||
},
|
||||
getOptionLabel( cat, query ) {
|
||||
const match = computeSuggestionMatch( cat.name, query ) || {};
|
||||
// @todo bring back ProductImage, but allow for product category image
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -38,6 +40,25 @@ export default {
|
|||
getOptionKeywords( coupon ) {
|
||||
return [ coupon.code ];
|
||||
},
|
||||
getFreeTextOptions( query ) {
|
||||
const label = (
|
||||
<span key="name" className="woocommerce-search__result-name">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __( 'All coupons with codes that include {{query /}}', 'wc-admin' ),
|
||||
components: {
|
||||
query: <strong className="components-form-token-field__suggestion-match">{ query }</strong>,
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
);
|
||||
const codeOption = {
|
||||
key: 'code',
|
||||
label: label,
|
||||
value: { id: query, code: query },
|
||||
};
|
||||
|
||||
return [ codeOption ];
|
||||
},
|
||||
getOptionLabel( coupon, query ) {
|
||||
const match = computeSuggestionMatch( coupon.code, query ) || {};
|
||||
return [
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -29,7 +31,7 @@ export default {
|
|||
let payload = '';
|
||||
if ( name ) {
|
||||
const query = {
|
||||
name,
|
||||
search: name,
|
||||
per_page: 10,
|
||||
};
|
||||
payload = stringifyQuery( query );
|
||||
|
@ -40,6 +42,25 @@ export default {
|
|||
getOptionKeywords( customer ) {
|
||||
return [ getName( customer ) ];
|
||||
},
|
||||
getFreeTextOptions( query ) {
|
||||
const label = (
|
||||
<span key="name" className="woocommerce-search__result-name">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __( 'All customers with names that include {{query /}}', 'wc-admin' ),
|
||||
components: {
|
||||
query: <strong className="components-form-token-field__suggestion-match">{ query }</strong>,
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
);
|
||||
const nameOption = {
|
||||
key: 'name',
|
||||
label: label,
|
||||
value: { id: query, first_name: query },
|
||||
};
|
||||
|
||||
return [ nameOption ];
|
||||
},
|
||||
getOptionLabel( customer, query ) {
|
||||
const match = computeSuggestionMatch( getName( customer ), query ) || {};
|
||||
return [
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -40,6 +42,25 @@ export default {
|
|||
getOptionKeywords( product ) {
|
||||
return [ product.name ];
|
||||
},
|
||||
getFreeTextOptions( query ) {
|
||||
const label = (
|
||||
<span key="name" className="woocommerce-search__result-name">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __( 'All products with titles that include {{query /}}', 'wc-admin' ),
|
||||
components: {
|
||||
query: <strong className="components-form-token-field__suggestion-match">{ query }</strong>,
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
);
|
||||
const titleOption = {
|
||||
key: 'title',
|
||||
label: label,
|
||||
value: { id: query, name: query },
|
||||
};
|
||||
|
||||
return [ titleOption ];
|
||||
},
|
||||
getOptionLabel( product, query ) {
|
||||
const match = computeSuggestionMatch( product.name, query ) || {};
|
||||
return [
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
|
@ -38,6 +40,25 @@ export default {
|
|||
getOptionKeywords( tax ) {
|
||||
return [ tax.id, getTaxCode( tax ) ];
|
||||
},
|
||||
getFreeTextOptions( query ) {
|
||||
const label = (
|
||||
<span key="name" className="woocommerce-search__result-name">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __( 'All taxes with codes that include {{query /}}', 'wc-admin' ),
|
||||
components: {
|
||||
query: <strong className="components-form-token-field__suggestion-match">{ query }</strong>,
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
);
|
||||
const codeOption = {
|
||||
key: 'code',
|
||||
label: label,
|
||||
value: { id: query, name: query },
|
||||
};
|
||||
|
||||
return [ codeOption ];
|
||||
},
|
||||
getOptionLabel( tax, query ) {
|
||||
const match = computeSuggestionMatch( getTaxCode( tax ), query ) || {};
|
||||
return [
|
||||
|
|
|
@ -27,12 +27,12 @@ export function computeSuggestionMatch( suggestion, query ) {
|
|||
|
||||
export function getTaxCode( tax ) {
|
||||
return [ tax.country, tax.state, tax.name || __( 'TAX', 'wc-admin' ), tax.priority ]
|
||||
.filter( Boolean )
|
||||
.map( item =>
|
||||
item
|
||||
.toString()
|
||||
.toUpperCase()
|
||||
.trim()
|
||||
)
|
||||
.filter( Boolean )
|
||||
.join( '-' );
|
||||
}
|
||||
|
|
|
@ -149,7 +149,7 @@ class Search extends Component {
|
|||
|
||||
render() {
|
||||
const autocompleter = this.getAutocompleter();
|
||||
const { placeholder, inlineTags, selected, instanceId, className, staticResults } = this.props;
|
||||
const { allowFreeTextSearch, placeholder, inlineTags, selected, instanceId, className, staticResults } = this.props;
|
||||
const { value = '', isActive } = this.state;
|
||||
const aria = {
|
||||
'aria-labelledby': this.props[ 'aria-labelledby' ],
|
||||
|
@ -164,6 +164,7 @@ class Search extends Component {
|
|||
'has-inline-tags': inlineTags,
|
||||
} ) }>
|
||||
<Autocomplete
|
||||
allowFreeText={ allowFreeTextSearch }
|
||||
completer={ autocompleter }
|
||||
onSelect={ this.selectResult }
|
||||
selected={ selected.map( s => s.id ) }
|
||||
|
@ -209,7 +210,7 @@ class Search extends Component {
|
|||
{ ...aria }
|
||||
/>
|
||||
<span id={ `search-inline-input-${ instanceId }` } className="screen-reader-text">
|
||||
{ __( 'Move backward for selected items' ) }
|
||||
{ __( 'Move backward for selected items', 'wc-admin' ) }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -238,6 +239,10 @@ class Search extends Component {
|
|||
}
|
||||
|
||||
Search.propTypes = {
|
||||
/**
|
||||
* Render additional options in the autocompleter to allow free text entering depending on the type.
|
||||
*/
|
||||
allowFreeTextSearch: PropTypes.bool,
|
||||
/**
|
||||
* Class name applied to parent div.
|
||||
*/
|
||||
|
@ -291,6 +296,7 @@ Search.propTypes = {
|
|||
};
|
||||
|
||||
Search.defaultProps = {
|
||||
allowFreeTextSearch: false,
|
||||
onChange: noop,
|
||||
selected: [],
|
||||
inlineTags: false,
|
||||
|
|
|
@ -48,7 +48,7 @@ class TableCard extends Component {
|
|||
const { compareBy, query } = props;
|
||||
|
||||
const showCols = props.headers.map( ( { key, hiddenByDefault } ) => ! hiddenByDefault && key ).filter( Boolean );
|
||||
const selectedRows = getIdsFromQuery( query[ compareBy ] );
|
||||
const selectedRows = query.filter ? getIdsFromQuery( query[ compareBy ] ) : [];
|
||||
|
||||
this.state = { showCols, selectedRows };
|
||||
this.onColumnToggle = this.onColumnToggle.bind( this );
|
||||
|
@ -61,14 +61,17 @@ class TableCard extends Component {
|
|||
|
||||
componentDidUpdate( { query: prevQuery, headers: prevHeaders } ) {
|
||||
const { compareBy, headers, query } = this.props;
|
||||
const prevIds = getIdsFromQuery( prevQuery[ compareBy ] );
|
||||
const currentIds = getIdsFromQuery( query[ compareBy ] );
|
||||
if ( ! isEqual( prevIds.sort(), currentIds.sort() ) ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( {
|
||||
selectedRows: currentIds,
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
|
||||
if ( query.filter || prevQuery.filter ) {
|
||||
const prevIds = prevQuery.filter ? getIdsFromQuery( prevQuery[ compareBy ] ) : [];
|
||||
const currentIds = query.filter ? getIdsFromQuery( query[ compareBy ] ) : [];
|
||||
if ( ! isEqual( prevIds.sort(), currentIds.sort() ) ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( {
|
||||
selectedRows: currentIds,
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
}
|
||||
if ( ! isEqual( headers, prevHeaders ) ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
|
@ -148,19 +151,17 @@ class TableCard extends Component {
|
|||
}
|
||||
|
||||
onSearch( values ) {
|
||||
const { compareBy, compareParam, onQueryChange, searchBy, searchParam } = this.props;
|
||||
const ids = values.map( v => v.id );
|
||||
if ( compareBy ) {
|
||||
const { selectedRows } = this.state;
|
||||
onQueryChange( 'compare' )(
|
||||
compareBy,
|
||||
compareParam,
|
||||
[ ...selectedRows, ...ids ].join( ',' )
|
||||
);
|
||||
} else if ( searchBy ) {
|
||||
const { compareParam } = this.props;
|
||||
const labels = values.map( v => v.label );
|
||||
if ( labels.length ) {
|
||||
updateQueryString( {
|
||||
filter: 'advanced',
|
||||
[ searchParam ]: ids.join( ',' ),
|
||||
filter: undefined,
|
||||
[ compareParam ]: undefined,
|
||||
search: uniq( labels ).join( ',' ),
|
||||
} );
|
||||
} else {
|
||||
updateQueryString( {
|
||||
search: undefined,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
@ -236,13 +237,14 @@ class TableCard extends Component {
|
|||
rowHeader,
|
||||
rowsPerPage,
|
||||
searchBy,
|
||||
searchParam,
|
||||
showMenu,
|
||||
summary,
|
||||
title,
|
||||
totalRows,
|
||||
} = this.props;
|
||||
const { selectedRows, showCols } = this.state;
|
||||
const searchedValues = query.search ? query.search.split( ',' ) : [];
|
||||
const searchedLabels = searchedValues.map( v => ( { id: v, label: v } ) );
|
||||
const allHeaders = this.props.headers;
|
||||
let headers = this.getVisibleHeaders();
|
||||
let rows = this.getVisibleRows();
|
||||
|
@ -277,13 +279,15 @@ class TableCard extends Component {
|
|||
{ labels.compareButton || __( 'Compare', 'wc-admin' ) }
|
||||
</CompareButton>
|
||||
),
|
||||
( compareBy || searchBy ) && (
|
||||
searchBy && (
|
||||
<Search
|
||||
allowFreeTextSearch={ true }
|
||||
inlineTags
|
||||
key="search"
|
||||
placeholder={ labels.placeholder || __( 'Search by item name', 'wc-admin' ) }
|
||||
type={ compareBy || searchBy }
|
||||
onChange={ this.onSearch }
|
||||
selected={ searchParam && getIdsFromQuery( query[ searchParam ] ).map( id => ( { id } ) ) }
|
||||
placeholder={ labels.placeholder || __( 'Search by item name', 'wc-admin' ) }
|
||||
selected={ searchedLabels }
|
||||
type={ searchBy }
|
||||
/>
|
||||
),
|
||||
( downloadable || onClickDownload ) && (
|
||||
|
@ -438,10 +442,6 @@ TableCard.propTypes = {
|
|||
* The string to use as a query parameter when searching row items.
|
||||
*/
|
||||
searchBy: PropTypes.string,
|
||||
/**
|
||||
* Url query parameter search function operates on
|
||||
*/
|
||||
searchParam: PropTypes.string,
|
||||
/**
|
||||
* Boolean to determine whether or not ellipsis menu is shown.
|
||||
*/
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.has-search {
|
||||
&.has-search:not(.has-compare) {
|
||||
.woocommerce-card__action {
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
|
|
Loading…
Reference in New Issue