woocommerce/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx

452 lines
12 KiB
TypeScript
Raw Normal View History

/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { COUNTRIES_STORE_NAME, Country } from '@woocommerce/data';
import { decodeEntities } from '@wordpress/html-entities';
import { escapeRegExp } from 'lodash';
import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
import { SelectControl, TextControl } from '@woocommerce/components';
import { Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { FormInputProps } from '~/utils/types';
const storeAddressFields = [
'addressLine1',
'addressLine2',
'city',
'countryState',
'postCode',
];
type Option = { key: string; label: string };
/**
* Check if a given address field is required for the locale.
*
* @param {string} fieldName Name of the field to check.
* @param {Object} locale Locale data.
* @return {boolean} Field requirement.
*/
export function isAddressFieldRequired(
fieldName: string,
locale: unknown = {}
): boolean {
if ( locale[ fieldName ]?.hasOwnProperty( 'required' ) ) {
return locale[ fieldName ]?.required as boolean;
}
if ( fieldName === 'address_2' ) {
return false;
}
return true;
}
/**
* Form validation.
*
* @param {Object} locale The store locale.
* @return {Function} Validator function.
*/
export function getStoreAddressValidator( locale = {} ) {
/**
* Form validator.
*
* @param {Object} values Keyed values of all fields in the form.
* @return {Object} Key value of fields and error messages, { myField: 'This field is required' }
*/
return ( values ) => {
const errors: {
[ key: string ]: string;
} = {};
if (
isAddressFieldRequired( 'address_1', locale ) &&
! values.addressLine1.trim().length
) {
errors.addressLine1 = __(
'Please add an address',
'woocommerce-admin'
);
}
if ( ! values.countryState.trim().length ) {
errors.countryState = __(
'Please select a country / region',
'woocommerce-admin'
);
}
if (
isAddressFieldRequired( 'city', locale ) &&
! values.city.trim().length
) {
errors.city = __( 'Please add a city', 'woocommerce-admin' );
}
if (
isAddressFieldRequired( 'postcode', locale ) &&
! values.postCode.trim().length
) {
errors.postCode = __(
'Please add a post code',
'woocommerce-admin'
);
}
return errors;
};
}
/**
* Get all country and state combinations used for select dropdowns.
*
* @return {Object} Select options, { value: 'US:GA', label: 'United States - Georgia' }
*/
export function getCountryStateOptions( countries: Country[] ) {
const countryStateOptions = countries.reduce( ( acc, country ) => {
if ( ! country.states.length ) {
acc.push( {
key: country.code,
label: decodeEntities( country.name ),
} );
return acc;
}
const countryStates = country.states.map( ( state ) => {
return {
key: country.code + ':' + state.code,
label:
decodeEntities( country.name ) +
' — ' +
decodeEntities( state.name ),
};
} );
acc.push( ...countryStates );
return acc;
}, [] );
return countryStateOptions;
}
/**
* Normalize state string for matching.
*
* @param {string} state The state to normalize.
* @return {Function} filter function.
*/
export const normalizeState = ( state: string ): string => {
return state.replace( /\s/g, '' ).toLowerCase();
};
/**
* Get state filter
*
* @param {string} isStateAbbreviation Whether to use state abbreviation or not.
* @param {string} normalizedAutofillState The value of the autofillState field.
* @return {Function} filter function.
*/
export const getStateFilter = (
isStateAbbreviation: boolean,
normalizedAutofillState: string
): ( ( option: Option ) => boolean ) => ( option: Option ) => {
const countryStateArray = isStateAbbreviation
? option.key.split( ':' )
: option.label.split( '—' );
// No region options in the country
if ( countryStateArray.length <= 1 ) {
return false;
}
const state = countryStateArray[ 1 ];
// Handle special case, for example: China — Beijing / 北京
if ( state.includes( '/' ) ) {
const stateStrList = state.split( '/' );
return (
normalizeState( stateStrList[ 0 ] ) === normalizedAutofillState ||
normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState
);
}
// Handle special case, for example: Iran — Alborz (البرز)
if ( state.includes( '(' ) && state.includes( ')' ) ) {
const stateStrList = state.replace( ')', '' ).split( '(' );
return (
normalizeState( stateStrList[ 0 ] ) === normalizedAutofillState ||
normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState
);
}
return normalizeState( state ) === normalizedAutofillState;
};
/**
* Get the autofill countryState fields and set value from filtered options.
*
* @param {Array} options Array of filterable options.
* @param {string} countryState The value of the countryState field.
* @param {Function} setValue Set value of the countryState input.
* @return {Object} React component.
*/
export function useGetCountryStateAutofill(
options: Option[],
countryState: string,
setValue: ( key: string, value: string ) => void
): JSX.Element {
const [ autofillCountry, setAutofillCountry ] = useState( '' );
const [ autofillState, setAutofillState ] = useState( '' );
const isAutofillChange: {
current: boolean;
} = useRef();
// Sync the autofill fields on first render and the countryState value changes.
useEffect( () => {
if ( ! isAutofillChange.current ) {
const option = options.find( ( opt ) => opt.key === countryState );
const labels = option
? option.label.split( /\u2013|\u2014|\-/ )
: [];
const newCountry = ( labels[ 0 ] || '' ).trim();
const newState = ( labels[ 1 ] || '' ).trim();
if (
newCountry !== autofillCountry ||
newState !== autofillState
) {
setAutofillCountry( newCountry );
setAutofillState( newState );
}
}
isAutofillChange.current = false;
// Disable reason: If we include autofillCountry/autofillState in the dependency array, we will have an unnecessary function call because we also update them in this function.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ countryState, options ] );
// Sync the countryState value the autofill fields changes
useEffect( () => {
// Skip on first render since we only want to update the value when the autofill fields changes.
if ( isAutofillChange.current === undefined ) {
return;
}
if ( ! autofillCountry && ! autofillState && countryState ) {
// Clear form
isAutofillChange.current = true;
setValue( 'countryState', '' );
return;
}
const countrySearch = new RegExp(
escapeRegExp( autofillCountry ),
'i'
);
const isCountryAbbreviation = autofillCountry.length < 3;
const isStateAbbreviation =
autofillState.length < 3 && !! autofillState.match( /^[\w]+$/ );
let filteredOptions = [];
if ( autofillCountry.length && autofillState.length ) {
filteredOptions = options.filter( ( option ) =>
countrySearch.test(
isCountryAbbreviation ? option.key : option.label
)
);
// no country matches so use all options for state filter.
if ( ! filteredOptions.length ) {
filteredOptions = [ ...options ];
}
if ( filteredOptions.length > 1 ) {
filteredOptions = filteredOptions.filter(
getStateFilter(
isStateAbbreviation,
normalizeState( autofillState )
)
);
}
} else if ( autofillCountry.length ) {
filteredOptions = options.filter( ( option ) =>
countrySearch.test(
isCountryAbbreviation ? option.key : option.label
)
);
} else if ( autofillState.length ) {
filteredOptions = options.filter(
getStateFilter(
isStateAbbreviation,
normalizeState( autofillState )
)
);
}
if (
filteredOptions.length === 1 &&
countryState !== filteredOptions[ 0 ].key
) {
isAutofillChange.current = true;
setValue( 'countryState', filteredOptions[ 0 ].key );
}
// Disable reason: If we include countryState in the dependency array, we will have an unnecessary function call because we also update it in this function.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ autofillCountry, autofillState, options, setValue ] );
return (
<>
<input
onChange={ ( event ) =>
setAutofillCountry( event.target.value )
}
value={ autofillCountry }
name="country"
type="text"
className="woocommerce-select-control__autofill-input"
tabIndex={ -1 }
autoComplete="country"
/>
<input
onChange={ ( event ) => setAutofillState( event.target.value ) }
value={ autofillState }
name="state"
type="text"
className="woocommerce-select-control__autofill-input"
tabIndex={ -1 }
autoComplete="address-level1"
/>
</>
);
}
type StoreAddressProps = {
getInputProps: ( key: string ) => FormInputProps;
setValue: ( key: string, value: string ) => void;
};
/**
* Store address fields.
*
* @param {Object} props Props for input components.
* @param {Function} props.getInputProps Get input props.
* @param {Function} props.setValue Set value of the countryState input.
* @return {Object} -
*/
export function StoreAddress( {
getInputProps,
setValue,
}: StoreAddressProps ): JSX.Element {
const countryState = getInputProps( 'countryState' ).value;
const {
locale,
hasFinishedResolution,
countries,
loadingCountries,
} = useSelect( ( select ) => {
const {
getLocale,
getCountries,
hasFinishedResolution: hasFinishedCountryResolution,
} = select( COUNTRIES_STORE_NAME );
return {
locale: getLocale( countryState ),
countries: getCountries(),
loadingCountries: ! hasFinishedCountryResolution( 'getCountries' ),
hasFinishedResolution: hasFinishedCountryResolution( 'getLocales' ),
};
} );
const countryStateOptions = useMemo(
() => getCountryStateOptions( countries ),
[ countries ]
);
const countryStateAutofill = useGetCountryStateAutofill(
countryStateOptions,
countryState,
setValue
);
useEffect( () => {
if ( locale ) {
storeAddressFields.forEach( ( field ) => {
const fieldKey = field
.replace( /(address)Line([0-9])/, '$1$2' )
.toLowerCase();
const props = getInputProps( field );
if ( locale[ fieldKey ]?.hidden && props.value?.length > 0 ) {
// Clear hidden field.
setValue( field, '' );
}
} );
}
}, [ countryState, locale ] );
if ( ! hasFinishedResolution || loadingCountries ) {
return <Spinner />;
}
return (
<div className="woocommerce-store-address-fields">
{ ! locale?.address_1?.hidden && (
<TextControl
label={
locale?.address_1?.label ||
__( 'Address line 1', 'woocommerce-admin' )
}
required={ isAddressFieldRequired( 'address_1', locale ) }
autoComplete="address-line1"
{ ...getInputProps( 'addressLine1' ) }
/>
) }
{ ! locale?.address_2?.hidden && (
<TextControl
label={
locale?.address_2?.label ||
__( 'Address line 2 (optional)', 'woocommerce-admin' )
}
required={ isAddressFieldRequired( 'address_2', locale ) }
autoComplete="address-line2"
{ ...getInputProps( 'addressLine2' ) }
/>
) }
<SelectControl
Merge final `version/1.0` branch with `master` (https://github.com/woocommerce/woocommerce-admin/pull/3848) * Try: Moving Customers to main Woo Menu (https://github.com/woocommerce/woocommerce-admin/pull/3632) * Only add onboarding settings on wc-admin pages when task list should be shown. (https://github.com/woocommerce/woocommerce-admin/pull/3722) * Use cron for unsnoozing admin notes (https://github.com/woocommerce/woocommerce-admin/pull/3662) * Use wp-cron for admin note snoozing. * Remove "unsnooze" scheduled action. * Use correct version. * Avoid using deprecated method for unscheduling actions. * Onboarding: Fix toggle tracking events (https://github.com/woocommerce/woocommerce-admin/pull/3645) * Fix errant wcadmin prefix on event name * Track the onboarding toggle on the option in case enable_onboarding isn't used * Move toggle actions to separate function * Move onboarding actions * Move onboarding filters * Move help tab updates to add_toggle_actions * Only run onboarding actions when enabled * Onboarding: Add tracks events when profiler steps are completed (https://github.com/woocommerce/woocommerce-admin/pull/3726) * Add tracks for store profiler step completion * Record event when profiler is completed * Ensure continue setup loads the onboarding profiler (https://github.com/woocommerce/woocommerce-admin/pull/3646) * 'All that include' option removed when input field is empty (https://github.com/woocommerce/woocommerce-admin/pull/3700) * 'All that include' option removed when input field is empty Added a control to check that when the input field 'Search by customer name' is empty, the 'All that include' option is not appearing. * Const name improved The constant name hasValues was changed to optionsHaveValues (more descriptive) * Fix select text alignment (https://github.com/woocommerce/woocommerce-admin/pull/3723) * Stock panel indicator - cache and use lookup tables. (https://github.com/woocommerce/woocommerce-admin/pull/3729) * Stock panel indicator - cache and use lookup tables. * Revise query, clear transient on product update. * Fix error, ht Josh. * Checklist: Remove sideloaded images to reduce build size, take 2 (https://github.com/woocommerce/woocommerce-admin/pull/3731) * Remove homepage template images. * Use other-small on all industries, adjust text color. * Remove background dim and opacity set to 0 * Fix/3631 (https://github.com/woocommerce/woocommerce-admin/pull/3730) * Added CBD as an industry type CBD was added as an industry type in API * Industries options modified Modified the industries options. Now we are able to choose if we will use an input or not in the option. * API control changed for industries. API control changed for industries. Now it accepts the data type we need. * Added input in Industries list for the option "Other" Added an input for the option "Other" in the industries list * Added suggested changes in review comments. * Added data preparation for recordEvent * Changed variable to snake_case The variable "industriesWithDetail" was changed to "industries_with_detail" (snake_case) * Onboarding: Create homepage without redirect (https://github.com/woocommerce/woocommerce-admin/pull/3727) * Add link to edit homepage instead of redirect * Add busy state to homepage creation button * Publish homepage on create via API * Update homepage notice to show on first post update * Update homepage creation notice per design * Record event on customize homepage * Set homepage to frontpage on creation * Add deactivation note for feature plugin (https://github.com/woocommerce/woocommerce-admin/pull/3687) * Add version deactivation note * Add the note to deactivate if the version is older than the current WC version * Deactivate wc admin feature plugin on action click * Add notes version hooks * change the Package class namespace to exclude from standalone autoloader * add use statement for FeaturePlugin * add note explaining namespace * use wc-admin-deactivate-plugin note name * Rename file and class to WC_Admin_Notes_Deactivate_Plugin Co-authored-by: Ron Rennick <ron@ronandandrea.com> Co-authored-by: Paul Sealock <psealock@gmail.com> * Add Travis tests on GH for release branch (https://github.com/woocommerce/woocommerce-admin/pull/3751) * Add Travis tests on GH for release branch * fix linter errors * ActivityPanels.php -> use public static functions * Remove free text Search option when no query exists (https://github.com/woocommerce/woocommerce-admin/pull/3755) * Revert changes in woocommerce/woocommerce-admin#3700 * Don't add free text search if no query exists * Add tests for Search without query * Add test for showing free text search option * Fix image sideloading for store industries. (https://github.com/woocommerce/woocommerce-admin/pull/3743) * Fix image sideloading for store industries. Data format changed in https://github.com/woocommerce/woocommerce-admin/pull/3730 * Fix industry image sideload in cases where the count is less than requested. * Be backwards compatible with the old industry data format. * Added event props to identify stores with WCS and Jetpack installed (https://github.com/woocommerce/woocommerce-admin/pull/3750) * Added event props to identify stores with WCS and Jetpack installed Also, added Jeckpack connected status * Improved variable name * Simplified method Simplified method. "intersection" check was removed * Tests errors repeared The method "clear_low_out_of_stock_count_transient" now is static. * OBW: fix sideloading image test error (https://github.com/woocommerce/woocommerce-admin/pull/3762) * Release 0.26.0 changes (https://github.com/woocommerce/woocommerce-admin/pull/3753) * add deactivation hook to Package.php (https://github.com/woocommerce/woocommerce-admin/pull/3770) * Add active version functions (https://github.com/woocommerce/woocommerce-admin/pull/3772) * add active version functions to Package.php Co-authored-by: Joshua T Flowers <joshuatf@gmail.com> * 0.26.1 changes (https://github.com/woocommerce/woocommerce-admin/pull/3773) * Customers Report: fix missing report param in search (https://github.com/woocommerce/woocommerce-admin/pull/3778) * Product titles include encoded entities (https://github.com/woocommerce/woocommerce-admin/pull/3765) * Stripped HTML from product titles and decoded before displaying them Stripped html from product titles and entities are decoded before displaying them * Stripped HTML from product titles and decoded in Stock report Stripped html from product titles and entities are decoded before displaying them. Now in Stock report * Added support for HTML tags and encoded entities on product titles Added support for HTML tags and encoded entities on filtered product list, dropdown menus and tag names. Also, strip_tags() function was replaced with wp_strip_all_tags() instead. * strip_tags() function was replaced with wp_strip_all_tags() instead. * Added control for a variable Added control for "item->data" before applying wp_strip_all_tags method. * pre-commit changes * Test text corrected * Enable taxes on automatic tax setup (https://github.com/woocommerce/woocommerce-admin/pull/3795) * Update Country Labeling to Match Core (https://github.com/woocommerce/woocommerce-admin/pull/3790) * Updated country labeling Country labeling on Customer Report was updated * Updated country labeling in other files * remove .jitm-card notice padding (https://github.com/woocommerce/woocommerce-admin/pull/3814) * OBW Connect: Fix requesting state (https://github.com/woocommerce/woocommerce-admin/pull/3786) * OBW Connect: Fix requesting state * pass down setIsPending * setIspending propType * defaultProps * test * Revert "test" This reverts commit e921092b19401931cc1aec8ee84fa53c53b67f36. * better comparison for redirect * Fixes Taxes Report search bug and adds initial documentation. (https://github.com/woocommerce/woocommerce-admin/pull/3816) * Initial Taxes Report documentation. * Fix taxes endpoint search parameter. * OBW: Fix retry plugin install button disappearing (https://github.com/woocommerce/woocommerce-admin/pull/3787) * OBW: Fix retry plugin install btn disappearing * try suggestion * Revert "try suggestion" This reverts commit 5b9386957a501ac3e729e5f16b0ee71c9d792859. * Fix special character escaping in search. (https://github.com/woocommerce/woocommerce-admin/pull/3826) * Properly prepare/escape special characters in Product search. * Properly prepare/escape special characters in Coupon search. * Properly prepare/escape special characters in Tax code search. * Fix tracking on migrated options (https://github.com/woocommerce/woocommerce-admin/pull/3828) * Don't track onboarding toggle if migrating options * Prevent WC_Tracks from recording event post types not yet registered * Activity Panels: Remove W Panel (https://github.com/woocommerce/woocommerce-admin/pull/3827) * Remove W Notif Panel. * Add back in trapping logic, and hide on non-embed pages. * add npm run test:zip command (https://github.com/woocommerce/woocommerce-admin/pull/3823) * add npm run test:zip command * 1.0.0 release changes🎉 (https://github.com/woocommerce/woocommerce-admin/pull/3831) * 1.0.0 release changes🎉 * changelog * 0.26.1 changelog * Add Report Extension Example: Add default props to ReportFilters (https://github.com/woocommerce/woocommerce-admin/pull/3830) * ReportFilters component: Add sane defaults * styles * add required column * add left join to sku ordering (https://github.com/woocommerce/woocommerce-admin/pull/3845) * Deal with lint errors, and improperly merged files * regenerate package-lock.json * attempting to resolve package lock conflict. Co-authored-by: Jeff Stieler <jeff.m.stieler@gmail.com> Co-authored-by: Joshua T Flowers <joshuatf@gmail.com> Co-authored-by: Ron Rennick <ron@ronandandrea.com> Co-authored-by: Fernando <ultimoround@gmail.com> Co-authored-by: edmundcwm <edmundcwm@gmail.com> Co-authored-by: Paul Sealock <psealock@gmail.com>
2020-03-10 02:47:39 +00:00
label={ __( 'Country / Region', 'woocommerce-admin' ) }
required
autoComplete="new-password" // disable autocomplete and autofill
options={ countryStateOptions }
excludeSelectedOptions={ false }
showAllOnFocus
isSearchable
{ ...getInputProps( 'countryState' ) }
controlClassName={ getInputProps( 'countryState' ).className }
>
{ countryStateAutofill }
</SelectControl>
{ ! locale?.city?.hidden && (
<TextControl
label={
locale?.city?.label || __( 'City', 'woocommerce-admin' )
}
required={ isAddressFieldRequired( 'city', locale ) }
{ ...getInputProps( 'city' ) }
autoComplete="address-level2"
/>
) }
{ ! locale?.postcode?.hidden && (
<TextControl
label={
locale?.postcode?.label ||
__( 'Post code', 'woocommerce-admin' )
}
required={ isAddressFieldRequired( 'postcode', locale ) }
autoComplete="postal-code"
{ ...getInputProps( 'postCode' ) }
/>
) }
</div>
);
}