Migrate `@woocommerce/components/search` to TS (#35724)

* Migrate search component to TS

* Add @types/prop-types to @woocommerce/components

* Update search types

* Add changelog

* Update pnpm-lock.yaml

* Update ts doc comments
This commit is contained in:
Chi-Hsuan Huang 2022-12-06 13:21:10 +08:00 committed by GitHub
parent 93b6d358f4
commit 2a56407ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1320 additions and 2169 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Migrate search component to TS

View File

@ -114,6 +114,7 @@
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.184",
"@types/prop-types": "^15.7.4",
"@types/react": "^17.0.2",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/wordpress__components": "^19.10.1",

View File

@ -1,171 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A product attributes completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'attributes',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'count',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products/attributes', query ),
} );
},
isDebounced: true,
getOptionIdentifier( attribute ) {
return attribute.id;
},
getOptionKeywords( attribute ) {
return [ attribute.name ];
},
getFreeTextOptions( query ) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{ interpolateComponents( {
mixedString: __(
'All attributes with names that include {{query /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const nameOption = {
key: 'name',
label,
value: { id: query, name: query },
};
return [ nameOption ];
},
getOptionLabel( attribute, query ) {
const match = computeSuggestionMatch( attribute.name, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ attribute.name }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( attribute ) {
const value = {
key: attribute.id,
label: attribute.name,
};
return value;
},
};

View File

@ -0,0 +1,92 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'attributes',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'count',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products/attributes', query ),
} );
},
isDebounced: true,
getOptionIdentifier( attribute ) {
return attribute.id;
},
getOptionKeywords( attribute ) {
return [ attribute.name ];
},
getFreeTextOptions( query ) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{ interpolateComponents( {
mixedString: __(
'All attributes with names that include {{query /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const nameOption = {
key: 'name',
label,
value: { id: query, name: query },
};
return [ nameOption ];
},
getOptionLabel( attribute, query ) {
const match = computeSuggestionMatch( attribute.name, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ attribute.name }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( attribute ) {
const value = {
key: attribute.id,
label: attribute.name,
};
return value;
},
};
export default completer;

View File

@ -1,171 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A product categories completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'categories',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'count',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products/categories', query ),
} );
},
isDebounced: true,
getOptionIdentifier( category ) {
return category.id;
},
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 /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const titleOption = {
key: 'title',
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
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ cat.name }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( cat ) {
const value = {
key: cat.id,
label: cat.name,
};
return value;
},
};

View File

@ -0,0 +1,92 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'categories',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'count',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products/categories', query ),
} );
},
isDebounced: true,
getOptionIdentifier( category ) {
return category.id;
},
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 /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const titleOption = {
key: 'title',
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
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ cat.name }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( cat ) {
const value = {
key: cat.id,
label: cat.name,
};
return value;
},
};
export default completer;

View File

@ -1,168 +0,0 @@
/**
* External dependencies
*/
import { decodeEntities } from '@wordpress/html-entities';
import { createElement, Fragment } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import Flag from '../../flag';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
// Cache countries to avoid repeated requests.
let allCountries = null;
/**
* A country completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'countries',
className: 'woocommerce-search__country-result',
isDebounced: true,
options() {
// Returned cached countries if we've already received them.
if ( allCountries ) {
return Promise.resolve( allCountries );
}
// Make the request for country data.
return apiFetch( { path: '/wc-analytics/data/countries' } ).then(
( result ) => {
// Cache the response.
allCountries = result;
return allCountries;
}
);
},
getOptionIdentifier( country ) {
return country.code;
},
getSearchExpression( query ) {
return '^' + query;
},
getOptionKeywords( country ) {
return [ country.code, decodeEntities( country.name ) ];
},
getOptionLabel( country, query ) {
const name = decodeEntities( country.name );
const match = computeSuggestionMatch( name, query ) || {};
return (
<Fragment>
<Flag
key="thumbnail"
className="woocommerce-search__result-thumbnail"
code={ country.code }
size={ 18 }
hideFromScreenReader
/>
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ name }
>
{ query ? (
<Fragment>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</Fragment>
) : (
name
) }
</span>
</Fragment>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( country ) {
const value = {
key: country.code,
label: decodeEntities( country.name ),
};
return value;
},
};

View File

@ -0,0 +1,108 @@
/**
* External dependencies
*/
import { decodeEntities } from '@wordpress/html-entities';
import { createElement, Fragment } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { Country } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import Flag from '../../flag';
import { AutoCompleter } from './types';
// Cache countries to avoid repeated requests.
let allCountries: Country[] | null = null;
const isCountries = ( value: unknown ): value is Country[] => {
return (
Array.isArray( value ) &&
value.length > 0 &&
typeof value[ 0 ] === 'object' &&
typeof value[ 0 ].code === 'string' &&
typeof value[ 0 ].name === 'string'
);
};
const completer: AutoCompleter = {
name: 'countries',
className: 'woocommerce-search__country-result',
isDebounced: true,
options() {
// Returned cached countries if we've already received them.
if ( allCountries ) {
return Promise.resolve( allCountries );
}
// Make the request for country data.
return apiFetch( { path: '/wc-analytics/data/countries' } ).then(
( result ) => {
if ( isCountries( result ) ) {
// Cache the response.
allCountries = result;
return allCountries;
}
// If the response is not valid, return an empty array.
// eslint-disable-next-line no-console
console.warn( 'Invalid countries response', result );
return [];
}
);
},
getOptionIdentifier( country ) {
return country.code;
},
getSearchExpression( query ) {
return '^' + query;
},
getOptionKeywords( country ) {
return [ country.code, decodeEntities( country.name ) ];
},
getOptionLabel( country, query ) {
const name = decodeEntities( country.name );
const match = computeSuggestionMatch( name, query );
return (
<Fragment>
{ /* @ts-expect-error TODO: migrate Flag component to TS. */ }
<Flag
key="thumbnail"
className="woocommerce-search__result-thumbnail"
code={ country.code }
// @ts-expect-error TODO: migrate Flag component.
size={ 18 }
hideFromScreenReader
/>
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ name }
>
{ query ? (
<Fragment>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</Fragment>
) : (
name
) }
</span>
</Fragment>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( country ) {
const value = {
key: country.code,
label: decodeEntities( country.name ),
};
return value;
},
};
export default completer;

View File

@ -1,169 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A coupon completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'coupons',
className: 'woocommerce-search__coupon-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/coupons', query ),
} );
},
isDebounced: true,
getOptionIdentifier( coupon ) {
return coupon.id;
},
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 /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const codeOption = {
key: 'code',
label,
value: { id: query, code: query },
};
return [ codeOption ];
},
getOptionLabel( coupon, query ) {
const match = computeSuggestionMatch( coupon.code, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ coupon.code }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( coupon ) {
const value = {
key: coupon.id,
label: coupon.code,
};
return value;
},
};

View File

@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'coupons',
className: 'woocommerce-search__coupon-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/coupons', query ),
} );
},
isDebounced: true,
getOptionIdentifier( coupon ) {
return coupon.id;
},
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 /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const codeOption = {
key: 'code',
label,
value: { id: query, code: query },
};
return [ codeOption ];
},
getOptionLabel( coupon, query ) {
const match = computeSuggestionMatch( coupon.code, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ coupon.code }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( coupon ) {
const value = {
key: coupon.id,
label: coupon.code,
};
return value;
},
};
export default completer;

View File

@ -1,169 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A customer completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'customers',
className: 'woocommerce-search__customers-result',
options( name ) {
const query = name
? {
search: name,
searchby: 'name',
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/customers', query ),
} );
},
isDebounced: true,
getOptionIdentifier( customer ) {
return customer.id;
},
getOptionKeywords( customer ) {
return [ customer.name ];
},
getFreeTextOptions( query ) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{ interpolateComponents( {
mixedString: __(
'All customers with names that include {{query /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const nameOption = {
key: 'name',
label,
value: { id: query, name: query },
};
return [ nameOption ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.name, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ customer.name }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
key: customer.id,
label: customer.name,
};
},
};

View File

@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'customers',
className: 'woocommerce-search__customers-result',
options( name ) {
const query = name
? {
search: name,
searchby: 'name',
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/customers', query ),
} );
},
isDebounced: true,
getOptionIdentifier( customer ) {
return customer.id;
},
getOptionKeywords( customer ) {
return [ customer.name ];
},
getFreeTextOptions( query ) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{ interpolateComponents( {
mixedString: __(
'All customers with names that include {{query /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const nameOption = {
key: 'name',
label,
value: { id: query, name: query },
};
return [ nameOption ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.name, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ customer.name }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
key: customer.id,
label: customer.name,
};
},
};
export default completer;

View File

@ -1,138 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A download IP address autocompleter.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'download-ips',
className: 'woocommerce-search__download-ip-result',
options( match ) {
const query = match
? {
match,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/data/download-ips', query ),
} );
},
isDebounced: true,
getOptionIdentifier( download ) {
return download.user_ip_address;
},
getOptionKeywords( download ) {
return [ download.user_ip_address ];
},
getOptionLabel( download, query ) {
const match =
computeSuggestionMatch( download.user_ip_address, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ download.user_ip_address }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
getOptionCompletion( download ) {
return {
key: download.user_ip_address,
label: download.user_ip_address,
};
},
};

View File

@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'download-ips',
className: 'woocommerce-search__download-ip-result',
options( match ) {
const query = match
? {
match,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/data/download-ips', query ),
} );
},
isDebounced: true,
getOptionIdentifier( download ) {
return download.user_ip_address;
},
getOptionKeywords( download ) {
return [ download.user_ip_address ];
},
getOptionLabel( download, query ) {
const match = computeSuggestionMatch( download.user_ip_address, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ download.user_ip_address }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
getOptionCompletion( download ) {
return {
key: download.user_ip_address,
label: download.user_ip_address,
};
},
};
export default completer;

View File

@ -1,141 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A customer email completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'emails',
className: 'woocommerce-search__emails-result',
options( search ) {
const query = search
? {
search,
searchby: 'email',
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/customers', query ),
} );
},
isDebounced: true,
getOptionIdentifier( customer ) {
return customer.id;
},
getOptionKeywords( customer ) {
return [ customer.email ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.email, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ customer.email }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
key: customer.id,
label: customer.email,
};
},
};

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'emails',
className: 'woocommerce-search__emails-result',
options( search ) {
const query = search
? {
search,
searchby: 'email',
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/customers', query ),
} );
},
isDebounced: true,
getOptionIdentifier( customer ) {
return customer.id;
},
getOptionKeywords( customer ) {
return [ customer.email ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.email, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ customer.email }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
key: customer.id,
label: customer.email,
};
},
};
export default completer;

View File

@ -14,3 +14,4 @@ export { default as taxes } from './taxes';
export { default as usernames } from './usernames';
export { default as variableProduct } from './variable-product';
export { default as variations } from './variations';
export * from './types';

View File

@ -1,138 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A orders autocompleter.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'orders',
className: 'woocommerce-search__order-result',
options( search ) {
const query = search
? {
number: search,
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/orders', query ),
} );
},
isDebounced: true,
getOptionIdentifier( order ) {
return order.id;
},
getOptionKeywords( order ) {
return [ '#' + order.number ];
},
getOptionLabel( order, query ) {
const match = computeSuggestionMatch( '#' + order.number, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ '#' + order.number }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
getOptionCompletion( order ) {
return {
key: order.id,
label: '#' + order.number,
};
},
};

View File

@ -0,0 +1,59 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'orders',
className: 'woocommerce-search__order-result',
options( search ) {
const query = search
? {
number: search,
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/orders', query ),
} );
},
isDebounced: true,
getOptionIdentifier( order ) {
return order.id;
},
getOptionKeywords( order ) {
return [ '#' + order.number ];
},
getOptionLabel( order, query ) {
const match = computeSuggestionMatch( '#' + order.number, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ '#' + order.number }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
getOptionCompletion( order ) {
return {
key: order.id,
label: '#' + order.number,
};
},
};
export default completer;

View File

@ -1,180 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement, Fragment } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import ProductImage from '../../product-image';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A products completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'products',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'popularity',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products', query ),
} );
},
isDebounced: true,
getOptionIdentifier( product ) {
return product.id;
},
getOptionKeywords( product ) {
return [ product.name, product.sku ];
},
getFreeTextOptions( query ) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{ interpolateComponents( {
mixedString: __(
'All products with titles that include {{query /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const titleOption = {
key: 'title',
label,
value: { id: query, name: query },
};
return [ titleOption ];
},
getOptionLabel( product, query ) {
const match = computeSuggestionMatch( product.name, query ) || {};
return (
<Fragment>
<ProductImage
key="thumbnail"
className="woocommerce-search__result-thumbnail"
product={ product }
width={ 18 }
height={ 18 }
alt=""
/>
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ product.name }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
</Fragment>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( product ) {
const value = {
key: product.id,
label: product.name,
};
return value;
},
};

View File

@ -0,0 +1,103 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement, Fragment } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import ProductImage from '../../product-image';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'products',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'popularity',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products', query ),
} );
},
isDebounced: true,
getOptionIdentifier( product ) {
return product.id;
},
getOptionKeywords( product ) {
return [ product.name, product.sku ];
},
getFreeTextOptions( query ) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{ interpolateComponents( {
mixedString: __(
'All products with titles that include {{query /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const titleOption = {
key: 'title',
label,
value: { id: query, name: query },
};
return [ titleOption ];
},
getOptionLabel( product, query ) {
const match = computeSuggestionMatch( product.name, query );
return (
<Fragment>
{ /* @ts-expect-error TODO: migrate ProductImage component to TS. */ }
<ProductImage
key="thumbnail"
className="woocommerce-search__result-thumbnail"
product={ product }
width={ 18 }
height={ 18 }
alt=""
/>
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ product.name }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
</Fragment>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( product ) {
const value = {
key: product.id,
label: product.name,
};
return value;
},
};
export default completer;

View File

@ -1,169 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch, getTaxCode } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A tax completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'taxes',
className: 'woocommerce-search__tax-result',
options( search ) {
const query = search
? {
code: search,
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/taxes', query ),
} );
},
isDebounced: true,
getOptionIdentifier( tax ) {
return tax.id;
},
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 /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const codeOption = {
key: 'code',
label,
value: { id: query, name: query },
};
return [ codeOption ];
},
getOptionLabel( tax, query ) {
const match = computeSuggestionMatch( getTaxCode( tax ), query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ tax.code }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( tax ) {
const value = {
key: tax.id,
label: getTaxCode( tax ),
};
return value;
},
};

View File

@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import interpolateComponents from '@automattic/interpolate-components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch, getTaxCode } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'taxes',
className: 'woocommerce-search__tax-result',
options( search ) {
const query = search
? {
code: search,
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/taxes', query ),
} );
},
isDebounced: true,
getOptionIdentifier( tax ) {
return tax.id;
},
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 /}}',
'woocommerce'
),
components: {
query: (
<strong className="components-form-token-field__suggestion-match">
{ query }
</strong>
),
},
} ) }
</span>
);
const codeOption = {
key: 'code',
label,
value: { id: query, name: query },
};
return [ codeOption ];
},
getOptionLabel( tax, query ) {
const match = computeSuggestionMatch( getTaxCode( tax ), query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ tax.code }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( tax ) {
const value = {
key: tax.id,
label: getTaxCode( tax ),
};
return value;
},
};
export default completer;

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { ReactNode, ReactElement } from 'react';
// Options may be of any type or shape.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CompleterOption = any;
export type FnGetOptions = (
query?: string
) => CompleterOption[] | Promise< CompleterOption[] >;
export type OptionCompletionValue = string | ReactElement | object;
export type OptionCompletion = {
/**
* The action declares what should be done with the value.
* There are currently two supported actions:
* 1. "insert-at-caret" - Insert the value into the text (the default completion action).
* 2. "replace" - Replace the current block with the block specified in the value property.
*/
action: 'insert-at-caret' | 'replace';
// The completion value.
value: OptionCompletionValue;
};
export type FnGetOptionCompletion = (
// The value of the completer option.
value: CompleterOption
) => OptionCompletion | OptionCompletionValue;
export type AutoCompleter = {
/* The name of the completer. Useful for identifying a specific completer to be overridden via extensibility hooks. */
name: string;
/* The raw options for completion. May be an array, a function that returns an array, or a function that returns a promise for an array. */
options: CompleterOption[] | FnGetOptions;
/* A function that returns a key to be used for the option. */
getOptionIdentifier: ( option: CompleterOption ) => string | number;
/* A function that returns the label for a given option. A label may be a string or a mixed array of strings, elements, and components. */
getOptionLabel: ( option: CompleterOption, query: string ) => ReactNode;
/* A function that takes an option and responds with how the option should be completed. By default, the result is a value to be inserted in the text. However, a completer may explicitly declare how a completion should be treated by returning an object with action and value properties. */
getOptionCompletion: FnGetOptionCompletion;
/* A function that returns the keywords for the specified option. */
getOptionKeywords: ( option: CompleterOption ) => string[];
/* A function that returns whether or not the specified option should be disabled. Disabled options cannot be selected. */
isOptionDisabled?: ( option: CompleterOption ) => boolean;
/* A function that takes a Range before and a Range after the autocomplete trigger and query text and returns a boolean indicating whether the completer should be considered for that context. */
allowContext?: ( before: string, after: string ) => boolean;
/* A function that returns options for the specified query. This is useful for filtering options based on the query. */
getFreeTextOptions?: ( query: string ) => [
{
key: string;
label: JSX.Element;
value: unknown;
}
];
/* A function to add regex expression to the filter the results, passed the search query. */
getSearchExpression?: ( query: string ) => string;
/* A class name to apply to the autocompletion popup menu. */
className?: string;
/* Whether to apply debouncing for the autocompleter. Set to true to enable debouncing. */
isDebounced?: boolean;
/* The input type for the search box control. */
inputType?: 'text' | 'search' | 'number' | 'email' | 'tel' | 'url';
};

View File

@ -1,141 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A customer username completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'usernames',
className: 'woocommerce-search__usernames-result',
options( search ) {
const query = search
? {
search,
searchby: 'username',
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/customers', query ),
} );
},
isDebounced: true,
getOptionIdentifier( customer ) {
return customer.id;
},
getOptionKeywords( customer ) {
return [ customer.username ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.username, query ) || {};
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ customer.username }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
key: customer.id,
label: customer.username,
};
},
};

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
name: 'usernames',
className: 'woocommerce-search__usernames-result',
options( search ) {
const query = search
? {
search,
searchby: 'username',
per_page: 10,
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/customers', query ),
} );
},
isDebounced: true,
getOptionIdentifier( customer ) {
return customer.id;
},
getOptionKeywords( customer ) {
return [ customer.username ];
},
getOptionLabel( customer, query ) {
const match = computeSuggestionMatch( customer.username, query );
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ customer.username }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( customer ) {
return {
key: customer.id,
label: customer.username,
};
},
};
export default completer;

View File

@ -12,7 +12,7 @@ import { decodeEntities } from '@wordpress/html-entities';
* @param {string} query The search term to match in the string.
* @return {Object} A list in three parts: before, match, and after.
*/
export function computeSuggestionMatch( suggestion, query ) {
export function computeSuggestionMatch( suggestion: string, query: string ) {
if ( ! query ) {
return null;
}
@ -33,7 +33,14 @@ export function computeSuggestionMatch( suggestion, query ) {
};
}
export function getTaxCode( tax ) {
type Tax = Partial< {
country: string;
state: string;
name: string;
priority: number;
} >;
export function getTaxCode( tax: Tax ) {
return [
tax.country,
tax.state,
@ -41,6 +48,6 @@ export function getTaxCode( tax ) {
tax.priority,
]
.filter( Boolean )
.map( ( item ) => item.toString().toUpperCase().trim() )
.map( ( item ) => item?.toString().toUpperCase().trim() )
.join( '-' );
}

View File

@ -1,109 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import productsAutocompleter from './product';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* A variable products completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
...productsAutocompleter,
name: 'products',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'popularity',
type: 'variable',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products', query ),
} );
},
};

View File

@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import productsAutocompleter from './product';
import { AutoCompleter } from './types';
const completer: AutoCompleter = {
...productsAutocompleter,
name: 'products',
options( search ) {
const query = search
? {
search,
per_page: 10,
orderby: 'popularity',
type: 'variable',
}
: {};
return apiFetch( {
path: addQueryArgs( '/wc-analytics/products', query ),
} );
},
};
export default completer;

View File

@ -1,204 +0,0 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement, Fragment } from '@wordpress/element';
import { getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import ProductImage from '../../product-image';
/**
* A raw completer option.
*
* @typedef {*} CompleterOption
*/
/**
* @callback FnGetOptions
*
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
*/
/**
* @callback FnGetOptionKeywords
* @param {CompleterOption} option a completer option.
*
* @return {string[]} list of key words to search.
*/
/**
* @callback FnIsOptionDisabled
* @param {CompleterOption} option a completer option.
*
* @return {string[]} whether or not the given option is disabled.
*/
/**
* @callback FnGetOptionLabel
* @param {CompleterOption} option a completer option.
*
* @return {(string|Array.<(string|Node)>)} list of react components to render.
*/
/**
* @callback FnAllowContext
* @param {string} before the string before the auto complete trigger and query.
* @param {string} after the string after the autocomplete trigger and query.
*
* @return {boolean} true if the completer can handle.
*/
/**
* @typedef {Object} OptionCompletion
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
* @property {OptionCompletionValue} value the completion value.
*/
/**
* A completion value.
*
* @typedef {(string|WPElement|Object)} OptionCompletionValue
*/
/**
* @callback FnGetOptionCompletion
* @param {CompleterOption} value the value of the completer option.
* @param {string} query the text value of the autocomplete query.
*
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
* OptionCompletionValue is returned, the
* completion action defaults to `insert-at-caret`.
*/
/**
* @typedef {Object} WPCompleter
* @property {string} name a way to identify a completer, useful for selective overriding.
* @property {?string} className A class to apply to the popup menu.
* @property {string} triggerPrefix the prefix that will display the menu.
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
*/
/**
* Create a variation name by concatenating each of the variation's
* attribute option strings.
*
* @param {Object} variation - variation returned by the api
* @param {Array} variation.attributes - attribute objects, with option property.
* @param {string} variation.name - name of variation.
* @return {string} - formatted variation name
*/
function getVariationName( { attributes, name } ) {
const separator =
window.wcSettings.variationTitleAttributesSeparator || ' - ';
if ( name.indexOf( separator ) > -1 ) {
return name;
}
const attributeList = attributes
.map( ( { option } ) => option )
.join( ', ' );
return attributeList ? name + separator + attributeList : name;
}
/**
* A variations completer.
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
*
* @type {WPCompleter}
*/
export default {
name: 'variations',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 30,
_fields: [
'attributes',
'description',
'id',
'name',
'sku',
],
}
: {};
const product = getQuery().products;
// Product was specified, search only its variations.
if ( product ) {
if ( product.includes( ',' ) ) {
// eslint-disable-next-line no-console
console.warn(
'Invalid product id supplied to Variations autocompleter'
);
}
return apiFetch( {
path: addQueryArgs(
`/wc-analytics/products/${ product }/variations`,
query
),
} );
}
// Product was not specified, search all variations.
return apiFetch( {
path: addQueryArgs( '/wc-analytics/variations', query ),
} );
},
isDebounced: true,
getOptionIdentifier( variation ) {
return variation.id;
},
getOptionKeywords( variation ) {
return [ getVariationName( variation ), variation.sku ];
},
getOptionLabel( variation, query ) {
const match =
computeSuggestionMatch( getVariationName( variation ), query ) ||
{};
return (
<Fragment>
<ProductImage
key="thumbnail"
className="woocommerce-search__result-thumbnail"
product={ variation }
width={ 18 }
height={ 18 }
alt=""
/>
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ variation.description }
>
{ match.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match.suggestionMatch }
</strong>
{ match.suggestionAfterMatch }
</span>
</Fragment>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( variation ) {
return {
key: variation.id,
label: getVariationName( variation ),
};
},
};

View File

@ -0,0 +1,133 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { createElement, Fragment } from '@wordpress/element';
import { getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { computeSuggestionMatch } from './utils';
import ProductImage from '../../product-image';
import { AutoCompleter } from './types';
/**
* Create a variation name by concatenating each of the variation's
* attribute option strings.
*
* @param {Object} variation - variation returned by the api
* @param {Array} variation.attributes - attribute objects, with option property.
* @param {string} variation.name - name of variation.
* @return {string} - formatted variation name
*/
function getVariationName( {
attributes,
name,
}: {
attributes: Array< { option: string } >;
name: string;
} ) {
const separator =
window.wcSettings.variationTitleAttributesSeparator || ' - ';
if ( name.indexOf( separator ) > -1 ) {
return name;
}
const attributeList = attributes
.map( ( { option } ) => option )
.join( ', ' );
return attributeList ? name + separator + attributeList : name;
}
const completer: AutoCompleter = {
name: 'variations',
className: 'woocommerce-search__product-result',
options( search ) {
const query = search
? {
search,
per_page: 30,
_fields: [
'attributes',
'description',
'id',
'name',
'sku',
],
}
: {};
const product = ( getQuery() as Record< string, string > ).products;
// Product was specified, search only its variations.
if ( product ) {
if ( product.includes( ',' ) ) {
// eslint-disable-next-line no-console
console.warn(
'Invalid product id supplied to Variations autocompleter'
);
}
return apiFetch( {
path: addQueryArgs(
`/wc-analytics/products/${ product }/variations`,
query
),
} );
}
// Product was not specified, search all variations.
return apiFetch( {
path: addQueryArgs( '/wc-analytics/variations', query ),
} );
},
isDebounced: true,
getOptionIdentifier( variation ) {
return variation.id;
},
getOptionKeywords( variation ) {
return [ getVariationName( variation ), variation.sku ];
},
getOptionLabel( variation, query ) {
const match = computeSuggestionMatch(
getVariationName( variation ),
query
);
return (
<Fragment>
{ /* @ts-expect-error TODO: migrate ProductImage component to TS. */ }
<ProductImage
key="thumbnail"
className="woocommerce-search__result-thumbnail"
product={ variation }
width={ 18 }
height={ 18 }
alt=""
/>
<span
key="name"
className="woocommerce-search__result-name"
aria-label={ variation.description }
>
{ match?.suggestionBeforeMatch }
<strong className="components-form-token-field__suggestion-match">
{ match?.suggestionMatch }
</strong>
{ match?.suggestionAfterMatch }
</span>
</Fragment>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion( variation ) {
return {
key: variation.id,
label: getVariationName( variation ),
};
},
};
export default completer;

View File

@ -24,14 +24,152 @@ import {
usernames,
variableProduct,
variations,
AutoCompleter,
OptionCompletionValue,
} from './autocompleters';
type Option = {
key: string | number;
label: React.ReactNode;
keywords: string[];
value: unknown;
};
type SearchType =
| 'attributes'
| 'categories'
| 'countries'
| 'coupons'
| 'customers'
| 'downloadIps'
| 'emails'
| 'orders'
| 'products'
| 'taxes'
| 'usernames'
| 'variableProducts'
| 'variations'
| 'custom';
type Props = {
type: SearchType;
allowFreeTextSearch?: boolean;
className?: string;
onChange?: ( value: Option | OptionCompletionValue[] ) => void;
autocompleter?: AutoCompleter;
placeholder?: string;
selected?:
| string
| Array< {
key: number | string;
label?: string;
} >;
inlineTags?: boolean;
showClearButton?: boolean;
staticResults?: boolean;
disabled?: boolean;
multiple?: boolean;
};
type State = {
options: unknown[];
};
/**
* A search box which autocompletes results while typing, allowing for the user to select an existing object
* (product, order, customer, etc). Currently only products are supported.
*/
export class Search extends Component {
constructor( props ) {
export class Search extends Component< Props, State > {
static 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.
*/
className: PropTypes.string,
/**
* Function called when selected results change, passed result list.
*/
onChange: PropTypes.func,
/**
* The object type to be used in searching.
*/
type: PropTypes.oneOf( [
'attributes',
'categories',
'countries',
'coupons',
'customers',
'downloadIps',
'emails',
'orders',
'products',
'taxes',
'usernames',
'variableProducts',
'variations',
'custom',
] ).isRequired,
/**
* The custom autocompleter to be used in searching when type is 'custom'
*/
autocompleter: PropTypes.object,
/**
* A placeholder for the search input.
*/
placeholder: PropTypes.string,
/**
* An array of objects describing selected values or optionally a string for a single value.
* If the label of the selected value is omitted, the Tag of that value will not
* be rendered inside the search box.
*/
selected: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.arrayOf(
PropTypes.shape( {
key: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ).isRequired,
label: PropTypes.string,
} )
),
] ),
/**
* Render tags inside input, otherwise render below input.
*/
inlineTags: PropTypes.bool,
/**
* Render a 'Clear' button next to the input box to remove its contents.
*/
showClearButton: PropTypes.bool,
/**
* Render results list positioned statically instead of absolutely.
*/
staticResults: PropTypes.bool,
/**
* Whether the control is disabled or not.
*/
disabled: PropTypes.bool,
/**
* Allow multiple option selections.
*/
multiple: PropTypes.bool,
};
static defaultProps = {
allowFreeTextSearch: false,
onChange: noop,
selected: [],
inlineTags: false,
showClearButton: false,
staticResults: false,
disabled: false,
multiple: true,
};
constructor( props: Props ) {
super( props );
this.state = {
options: [],
@ -80,13 +218,15 @@ export class Search extends Component {
}
return this.props.autocompleter;
default:
return {};
throw new Error(
`No autocompleter found for type: ${ this.props.type }`
);
}
}
getFormattedOptions( options, query ) {
getFormattedOptions( options: unknown[], query: string ) {
const autocompleter = this.getAutocompleter();
const formattedOptions = [];
const formattedOptions: Option[] = [];
options.forEach( ( option ) => {
const formattedOption = {
@ -103,7 +243,7 @@ export class Search extends Component {
return formattedOptions;
}
fetchOptions( previousOptions, query ) {
fetchOptions( previousOptions: unknown[], query: string ) {
if ( ! query ) {
return [];
}
@ -123,11 +263,14 @@ export class Search extends Component {
} );
}
updateSelected( selected ) {
const { onChange } = this.props;
updateSelected( selected: Option[] ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { onChange = ( _option: unknown[] ) => {} } = this.props;
const autocompleter = this.getAutocompleter();
const formattedSelections = selected.map( ( option ) => {
const formattedSelections = selected.map<
Option | OptionCompletionValue
>( ( option ) => {
return option.value
? autocompleter.getOptionCompletion( option.value )
: option;
@ -136,17 +279,21 @@ export class Search extends Component {
onChange( formattedSelections );
}
appendFreeTextSearch( options, query ) {
appendFreeTextSearch( options: unknown[], query: string ) {
const { allowFreeTextSearch } = this.props;
if ( ! query || ! query.length ) {
return [];
}
if ( ! allowFreeTextSearch ) {
const autocompleter = this.getAutocompleter();
if (
! allowFreeTextSearch ||
typeof autocompleter.getFreeTextOptions !== 'function'
) {
return options;
}
const autocompleter = this.getAutocompleter();
return [ ...autocompleter.getFreeTextOptions( query ), ...options ];
}
@ -195,90 +342,4 @@ export 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.
*/
className: PropTypes.string,
/**
* Function called when selected results change, passed result list.
*/
onChange: PropTypes.func,
/**
* The object type to be used in searching.
*/
type: PropTypes.oneOf( [
'attributes',
'categories',
'countries',
'coupons',
'customers',
'downloadIps',
'emails',
'orders',
'products',
'taxes',
'usernames',
'variableProducts',
'variations',
'custom',
] ).isRequired,
/**
* The custom autocompleter to be used in searching when type is 'custom'
*/
autocompleter: PropTypes.object,
/**
* A placeholder for the search input.
*/
placeholder: PropTypes.string,
/**
* An array of objects describing selected values or optionally a string for a single value.
* If the label of the selected value is omitted, the Tag of that value will not
* be rendered inside the search box.
*/
selected: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.arrayOf(
PropTypes.shape( {
key: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ).isRequired,
label: PropTypes.string,
} )
),
] ),
/**
* Render tags inside input, otherwise render below input.
*/
inlineTags: PropTypes.bool,
/**
* Render a 'Clear' button next to the input box to remove its contents.
*/
showClearButton: PropTypes.bool,
/**
* Render results list positioned statically instead of absolutely.
*/
staticResults: PropTypes.bool,
/**
* Whether the control is disabled or not.
*/
disabled: PropTypes.bool,
};
Search.defaultProps = {
allowFreeTextSearch: false,
onChange: noop,
selected: [],
inlineTags: false,
showClearButton: false,
staticResults: false,
disabled: false,
multiple: true,
};
export default Search;

View File

@ -0,0 +1,11 @@
declare global {
interface Window {
wcSettings: {
variationTitleAttributesSeparator?: string;
};
}
}
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export {};

View File

@ -203,6 +203,7 @@ importers:
'@testing-library/user-event': ^13.5.0
'@types/jest': ^27.4.1
'@types/lodash': ^4.14.184
'@types/prop-types': ^15.7.4
'@types/react': ^17.0.2
'@types/testing-library__jest-dom': ^5.14.3
'@types/wordpress__block-editor': ^7.0.0
@ -357,6 +358,7 @@ importers:
'@testing-library/user-event': 13.5.0_gzufz4q333be4gqfrvipwvqt6a
'@types/jest': 27.4.1
'@types/lodash': 4.14.184
'@types/prop-types': 15.7.5
'@types/react': 17.0.50
'@types/testing-library__jest-dom': 5.14.3
'@types/wordpress__components': 19.10.1_sfoxds7t5ydpegc3knd667wn6m