From 2a56407ba125ab281f901817af2485438c18a9b0 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 6 Dec 2022 13:21:10 +0800 Subject: [PATCH] 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 --- .../dev-migrate-search-component-to-ts | 4 + packages/js/components/package.json | 1 + .../src/search/autocompleters/attributes.js | 171 ------------ .../src/search/autocompleters/attributes.tsx | 92 +++++++ .../src/search/autocompleters/categories.js | 171 ------------ .../src/search/autocompleters/categories.tsx | 92 +++++++ .../src/search/autocompleters/countries.js | 168 ------------ .../src/search/autocompleters/countries.tsx | 108 ++++++++ .../src/search/autocompleters/coupons.js | 169 ------------ .../src/search/autocompleters/coupons.tsx | 90 ++++++ .../src/search/autocompleters/customers.js | 169 ------------ .../src/search/autocompleters/customers.tsx | 90 ++++++ .../src/search/autocompleters/download-ips.js | 138 ---------- .../search/autocompleters/download-ips.tsx | 58 ++++ .../src/search/autocompleters/emails.js | 141 ---------- .../src/search/autocompleters/emails.tsx | 62 +++++ .../autocompleters/{index.js => index.ts} | 1 + .../src/search/autocompleters/orders.js | 138 ---------- .../src/search/autocompleters/orders.tsx | 59 ++++ .../src/search/autocompleters/product.js | 180 ------------ .../src/search/autocompleters/product.tsx | 103 +++++++ .../src/search/autocompleters/taxes.js | 169 ------------ .../src/search/autocompleters/taxes.tsx | 90 ++++++ .../src/search/autocompleters/types.ts | 62 +++++ .../src/search/autocompleters/usernames.js | 141 ---------- .../src/search/autocompleters/usernames.tsx | 62 +++++ .../autocompleters/{utils.js => utils.ts} | 13 +- .../search/autocompleters/variable-product.js | 109 -------- .../autocompleters/variable-product.tsx | 31 +++ .../src/search/autocompleters/variations.js | 204 -------------- .../src/search/autocompleters/variations.tsx | 133 +++++++++ .../src/search/{index.js => index.tsx} | 257 +++++++++++------- packages/js/components/typings/global.d.ts | 11 + pnpm-lock.yaml | 2 + 34 files changed, 1320 insertions(+), 2169 deletions(-) create mode 100644 packages/js/components/changelog/dev-migrate-search-component-to-ts delete mode 100644 packages/js/components/src/search/autocompleters/attributes.js create mode 100644 packages/js/components/src/search/autocompleters/attributes.tsx delete mode 100644 packages/js/components/src/search/autocompleters/categories.js create mode 100644 packages/js/components/src/search/autocompleters/categories.tsx delete mode 100644 packages/js/components/src/search/autocompleters/countries.js create mode 100644 packages/js/components/src/search/autocompleters/countries.tsx delete mode 100644 packages/js/components/src/search/autocompleters/coupons.js create mode 100644 packages/js/components/src/search/autocompleters/coupons.tsx delete mode 100644 packages/js/components/src/search/autocompleters/customers.js create mode 100644 packages/js/components/src/search/autocompleters/customers.tsx delete mode 100644 packages/js/components/src/search/autocompleters/download-ips.js create mode 100644 packages/js/components/src/search/autocompleters/download-ips.tsx delete mode 100644 packages/js/components/src/search/autocompleters/emails.js create mode 100644 packages/js/components/src/search/autocompleters/emails.tsx rename packages/js/components/src/search/autocompleters/{index.js => index.ts} (96%) delete mode 100644 packages/js/components/src/search/autocompleters/orders.js create mode 100644 packages/js/components/src/search/autocompleters/orders.tsx delete mode 100644 packages/js/components/src/search/autocompleters/product.js create mode 100644 packages/js/components/src/search/autocompleters/product.tsx delete mode 100644 packages/js/components/src/search/autocompleters/taxes.js create mode 100644 packages/js/components/src/search/autocompleters/taxes.tsx create mode 100644 packages/js/components/src/search/autocompleters/types.ts delete mode 100644 packages/js/components/src/search/autocompleters/usernames.js create mode 100644 packages/js/components/src/search/autocompleters/usernames.tsx rename packages/js/components/src/search/autocompleters/{utils.js => utils.ts} (79%) delete mode 100644 packages/js/components/src/search/autocompleters/variable-product.js create mode 100644 packages/js/components/src/search/autocompleters/variable-product.tsx delete mode 100644 packages/js/components/src/search/autocompleters/variations.js create mode 100644 packages/js/components/src/search/autocompleters/variations.tsx rename packages/js/components/src/search/{index.js => index.tsx} (53%) create mode 100644 packages/js/components/typings/global.d.ts diff --git a/packages/js/components/changelog/dev-migrate-search-component-to-ts b/packages/js/components/changelog/dev-migrate-search-component-to-ts new file mode 100644 index 00000000000..ded2b14cc92 --- /dev/null +++ b/packages/js/components/changelog/dev-migrate-search-component-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate search component to TS diff --git a/packages/js/components/package.json b/packages/js/components/package.json index 6067f5a04d3..a663f1f123d 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -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", diff --git a/packages/js/components/src/search/autocompleters/attributes.js b/packages/js/components/src/search/autocompleters/attributes.js deleted file mode 100644 index e9870af472d..00000000000 --- a/packages/js/components/src/search/autocompleters/attributes.js +++ /dev/null @@ -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.)} 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 = ( - - { interpolateComponents( { - mixedString: __( - 'All attributes with names that include {{query /}}', - 'woocommerce' - ), - components: { - query: ( - - { query } - - ), - }, - } ) } - - ); - const nameOption = { - key: 'name', - label, - value: { id: query, name: query }, - }; - - return [ nameOption ]; - }, - getOptionLabel( attribute, query ) { - const match = computeSuggestionMatch( attribute.name, query ) || {}; - - return ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/attributes.tsx b/packages/js/components/src/search/autocompleters/attributes.tsx new file mode 100644 index 00000000000..4cec0878fe8 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/attributes.tsx @@ -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 = ( + + { interpolateComponents( { + mixedString: __( + 'All attributes with names that include {{query /}}', + 'woocommerce' + ), + components: { + query: ( + + { query } + + ), + }, + } ) } + + ); + const nameOption = { + key: 'name', + label, + value: { id: query, name: query }, + }; + + return [ nameOption ]; + }, + getOptionLabel( attribute, query ) { + const match = computeSuggestionMatch( attribute.name, query ); + + return ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/categories.js b/packages/js/components/src/search/autocompleters/categories.js deleted file mode 100644 index 435b52101ee..00000000000 --- a/packages/js/components/src/search/autocompleters/categories.js +++ /dev/null @@ -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.)} 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 = ( - - { interpolateComponents( { - mixedString: __( - 'All categories with titles that include {{query /}}', - 'woocommerce' - ), - components: { - query: ( - - { query } - - ), - }, - } ) } - - ); - 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 ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/categories.tsx b/packages/js/components/src/search/autocompleters/categories.tsx new file mode 100644 index 00000000000..9b022bcb1f6 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/categories.tsx @@ -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 = ( + + { interpolateComponents( { + mixedString: __( + 'All categories with titles that include {{query /}}', + 'woocommerce' + ), + components: { + query: ( + + { query } + + ), + }, + } ) } + + ); + 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 ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/countries.js b/packages/js/components/src/search/autocompleters/countries.js deleted file mode 100644 index 114b9ae96a8..00000000000 --- a/packages/js/components/src/search/autocompleters/countries.js +++ /dev/null @@ -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.)} 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 ( - - - - { query ? ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ) : ( - name - ) } - - - ); - }, - // 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; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/countries.tsx b/packages/js/components/src/search/autocompleters/countries.tsx new file mode 100644 index 00000000000..c60586a02cc --- /dev/null +++ b/packages/js/components/src/search/autocompleters/countries.tsx @@ -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 ( + + { /* @ts-expect-error TODO: migrate Flag component to TS. */ } + + + { query ? ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ) : ( + name + ) } + + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/coupons.js b/packages/js/components/src/search/autocompleters/coupons.js deleted file mode 100644 index 4764f2495b3..00000000000 --- a/packages/js/components/src/search/autocompleters/coupons.js +++ /dev/null @@ -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.)} 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 = ( - - { interpolateComponents( { - mixedString: __( - 'All coupons with codes that include {{query /}}', - 'woocommerce' - ), - components: { - query: ( - - { query } - - ), - }, - } ) } - - ); - const codeOption = { - key: 'code', - label, - value: { id: query, code: query }, - }; - - return [ codeOption ]; - }, - getOptionLabel( coupon, query ) { - const match = computeSuggestionMatch( coupon.code, query ) || {}; - return ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/coupons.tsx b/packages/js/components/src/search/autocompleters/coupons.tsx new file mode 100644 index 00000000000..4d6dd536157 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/coupons.tsx @@ -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 = ( + + { interpolateComponents( { + mixedString: __( + 'All coupons with codes that include {{query /}}', + 'woocommerce' + ), + components: { + query: ( + + { query } + + ), + }, + } ) } + + ); + const codeOption = { + key: 'code', + label, + value: { id: query, code: query }, + }; + + return [ codeOption ]; + }, + getOptionLabel( coupon, query ) { + const match = computeSuggestionMatch( coupon.code, query ); + return ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/customers.js b/packages/js/components/src/search/autocompleters/customers.js deleted file mode 100644 index 7d33f8669fd..00000000000 --- a/packages/js/components/src/search/autocompleters/customers.js +++ /dev/null @@ -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.)} 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 = ( - - { interpolateComponents( { - mixedString: __( - 'All customers with names that include {{query /}}', - 'woocommerce' - ), - components: { - query: ( - - { query } - - ), - }, - } ) } - - ); - const nameOption = { - key: 'name', - label, - value: { id: query, name: query }, - }; - - return [ nameOption ]; - }, - getOptionLabel( customer, query ) { - const match = computeSuggestionMatch( customer.name, query ) || {}; - return ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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, - }; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/customers.tsx b/packages/js/components/src/search/autocompleters/customers.tsx new file mode 100644 index 00000000000..2e82c678f70 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/customers.tsx @@ -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 = ( + + { interpolateComponents( { + mixedString: __( + 'All customers with names that include {{query /}}', + 'woocommerce' + ), + components: { + query: ( + + { query } + + ), + }, + } ) } + + ); + const nameOption = { + key: 'name', + label, + value: { id: query, name: query }, + }; + + return [ nameOption ]; + }, + getOptionLabel( customer, query ) { + const match = computeSuggestionMatch( customer.name, query ); + return ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/download-ips.js b/packages/js/components/src/search/autocompleters/download-ips.js deleted file mode 100644 index 62c76952adf..00000000000 --- a/packages/js/components/src/search/autocompleters/download-ips.js +++ /dev/null @@ -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.)} 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 ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - getOptionCompletion( download ) { - return { - key: download.user_ip_address, - label: download.user_ip_address, - }; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/download-ips.tsx b/packages/js/components/src/search/autocompleters/download-ips.tsx new file mode 100644 index 00000000000..9dcc1862720 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/download-ips.tsx @@ -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 ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + getOptionCompletion( download ) { + return { + key: download.user_ip_address, + label: download.user_ip_address, + }; + }, +}; + +export default completer; diff --git a/packages/js/components/src/search/autocompleters/emails.js b/packages/js/components/src/search/autocompleters/emails.js deleted file mode 100644 index c6d689bdaac..00000000000 --- a/packages/js/components/src/search/autocompleters/emails.js +++ /dev/null @@ -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.)} 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 ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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, - }; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/emails.tsx b/packages/js/components/src/search/autocompleters/emails.tsx new file mode 100644 index 00000000000..d2b6e116f9f --- /dev/null +++ b/packages/js/components/src/search/autocompleters/emails.tsx @@ -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 ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/index.js b/packages/js/components/src/search/autocompleters/index.ts similarity index 96% rename from packages/js/components/src/search/autocompleters/index.js rename to packages/js/components/src/search/autocompleters/index.ts index 30d13e5d291..072af7163af 100644 --- a/packages/js/components/src/search/autocompleters/index.js +++ b/packages/js/components/src/search/autocompleters/index.ts @@ -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'; diff --git a/packages/js/components/src/search/autocompleters/orders.js b/packages/js/components/src/search/autocompleters/orders.js deleted file mode 100644 index 06ee794f198..00000000000 --- a/packages/js/components/src/search/autocompleters/orders.js +++ /dev/null @@ -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.)} 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 ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - getOptionCompletion( order ) { - return { - key: order.id, - label: '#' + order.number, - }; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/orders.tsx b/packages/js/components/src/search/autocompleters/orders.tsx new file mode 100644 index 00000000000..3baac31f0c6 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/orders.tsx @@ -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 ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + getOptionCompletion( order ) { + return { + key: order.id, + label: '#' + order.number, + }; + }, +}; + +export default completer; diff --git a/packages/js/components/src/search/autocompleters/product.js b/packages/js/components/src/search/autocompleters/product.js deleted file mode 100644 index 2c8046fd9fa..00000000000 --- a/packages/js/components/src/search/autocompleters/product.js +++ /dev/null @@ -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.)} 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 = ( - - { interpolateComponents( { - mixedString: __( - 'All products with titles that include {{query /}}', - 'woocommerce' - ), - components: { - query: ( - - { query } - - ), - }, - } ) } - - ); - const titleOption = { - key: 'title', - label, - value: { id: query, name: query }, - }; - - return [ titleOption ]; - }, - getOptionLabel( product, query ) { - const match = computeSuggestionMatch( product.name, query ) || {}; - return ( - - - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - - ); - }, - // 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; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/product.tsx b/packages/js/components/src/search/autocompleters/product.tsx new file mode 100644 index 00000000000..44e70524063 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/product.tsx @@ -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 = ( + + { interpolateComponents( { + mixedString: __( + 'All products with titles that include {{query /}}', + 'woocommerce' + ), + components: { + query: ( + + { query } + + ), + }, + } ) } + + ); + const titleOption = { + key: 'title', + label, + value: { id: query, name: query }, + }; + + return [ titleOption ]; + }, + getOptionLabel( product, query ) { + const match = computeSuggestionMatch( product.name, query ); + return ( + + { /* @ts-expect-error TODO: migrate ProductImage component to TS. */ } + + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/taxes.js b/packages/js/components/src/search/autocompleters/taxes.js deleted file mode 100644 index 8a3341e24f3..00000000000 --- a/packages/js/components/src/search/autocompleters/taxes.js +++ /dev/null @@ -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.)} 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 = ( - - { interpolateComponents( { - mixedString: __( - 'All taxes with codes that include {{query /}}', - 'woocommerce' - ), - components: { - query: ( - - { query } - - ), - }, - } ) } - - ); - const codeOption = { - key: 'code', - label, - value: { id: query, name: query }, - }; - - return [ codeOption ]; - }, - getOptionLabel( tax, query ) { - const match = computeSuggestionMatch( getTaxCode( tax ), query ) || {}; - return ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/taxes.tsx b/packages/js/components/src/search/autocompleters/taxes.tsx new file mode 100644 index 00000000000..811ef848503 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/taxes.tsx @@ -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 = ( + + { interpolateComponents( { + mixedString: __( + 'All taxes with codes that include {{query /}}', + 'woocommerce' + ), + components: { + query: ( + + { query } + + ), + }, + } ) } + + ); + const codeOption = { + key: 'code', + label, + value: { id: query, name: query }, + }; + + return [ codeOption ]; + }, + getOptionLabel( tax, query ) { + const match = computeSuggestionMatch( getTaxCode( tax ), query ); + return ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/types.ts b/packages/js/components/src/search/autocompleters/types.ts new file mode 100644 index 00000000000..8c2808f4e2a --- /dev/null +++ b/packages/js/components/src/search/autocompleters/types.ts @@ -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'; +}; diff --git a/packages/js/components/src/search/autocompleters/usernames.js b/packages/js/components/src/search/autocompleters/usernames.js deleted file mode 100644 index 9899b46ea56..00000000000 --- a/packages/js/components/src/search/autocompleters/usernames.js +++ /dev/null @@ -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.)} 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 ( - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - ); - }, - // 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, - }; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/usernames.tsx b/packages/js/components/src/search/autocompleters/usernames.tsx new file mode 100644 index 00000000000..a0c2b711c7f --- /dev/null +++ b/packages/js/components/src/search/autocompleters/usernames.tsx @@ -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 ( + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + ); + }, + // 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; diff --git a/packages/js/components/src/search/autocompleters/utils.js b/packages/js/components/src/search/autocompleters/utils.ts similarity index 79% rename from packages/js/components/src/search/autocompleters/utils.js rename to packages/js/components/src/search/autocompleters/utils.ts index 792e7bd4811..06d3cf4c831 100644 --- a/packages/js/components/src/search/autocompleters/utils.js +++ b/packages/js/components/src/search/autocompleters/utils.ts @@ -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( '-' ); } diff --git a/packages/js/components/src/search/autocompleters/variable-product.js b/packages/js/components/src/search/autocompleters/variable-product.js deleted file mode 100644 index 7179ba99a90..00000000000 --- a/packages/js/components/src/search/autocompleters/variable-product.js +++ /dev/null @@ -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.)} 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 ), - } ); - }, -}; diff --git a/packages/js/components/src/search/autocompleters/variable-product.tsx b/packages/js/components/src/search/autocompleters/variable-product.tsx new file mode 100644 index 00000000000..2e72c92c773 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/variable-product.tsx @@ -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; diff --git a/packages/js/components/src/search/autocompleters/variations.js b/packages/js/components/src/search/autocompleters/variations.js deleted file mode 100644 index 82f141d7887..00000000000 --- a/packages/js/components/src/search/autocompleters/variations.js +++ /dev/null @@ -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.)} 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 ( - - - - { match.suggestionBeforeMatch } - - { match.suggestionMatch } - - { match.suggestionAfterMatch } - - - ); - }, - // 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 ), - }; - }, -}; diff --git a/packages/js/components/src/search/autocompleters/variations.tsx b/packages/js/components/src/search/autocompleters/variations.tsx new file mode 100644 index 00000000000..ba4c38c02f3 --- /dev/null +++ b/packages/js/components/src/search/autocompleters/variations.tsx @@ -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 ( + + { /* @ts-expect-error TODO: migrate ProductImage component to TS. */ } + + + { match?.suggestionBeforeMatch } + + { match?.suggestionMatch } + + { match?.suggestionAfterMatch } + + + ); + }, + // 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; diff --git a/packages/js/components/src/search/index.js b/packages/js/components/src/search/index.tsx similarity index 53% rename from packages/js/components/src/search/index.js rename to packages/js/components/src/search/index.tsx index 7cbdf093313..b4558f62499 100644 --- a/packages/js/components/src/search/index.js +++ b/packages/js/components/src/search/index.tsx @@ -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; diff --git a/packages/js/components/typings/global.d.ts b/packages/js/components/typings/global.d.ts new file mode 100644 index 00000000000..cc7c2541f01 --- /dev/null +++ b/packages/js/components/typings/global.d.ts @@ -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 {}; + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8d78f52f13..240966777a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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