* Move payment methods

* Setup entrypoints

* Sortable implementation

* Sortable

* Basic UI in place for settings

* Hydrate real settings

* Form updates values

* Styling and save button placement

* useSettings hook

* Prepare for save

* delete

* Add location button

* Remove className

* Conditional display of taxes

* Save via API

* Update general settings to designs

* Modal styles

* Style table

* Border colors and radius

* Added e2e tests

* use node 16

* Enqueue states in admin

* Use render from wordpress/element

* Missing handle style

* Enable translations

* Remove curried function

* Todo for inline settings

Co-authored-by: Nadir Seghir <nadir.seghir@gmail.com>
This commit is contained in:
Mike Jolley 2022-11-11 16:17:49 +00:00 committed by Nadir Seghir
parent 3d0109f1dc
commit c49426570d
32 changed files with 2157 additions and 401 deletions

View File

@ -0,0 +1,133 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { SelectControl, TextControl } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import type { PickupLocation } from '../types';
import StateControl from './state-control';
const Form = ( {
formRef,
values,
setValues,
}: {
formRef: React.RefObject< HTMLFormElement >;
values: PickupLocation;
setValues: React.Dispatch< React.SetStateAction< PickupLocation > >;
} ) => {
const countries = getSetting< Record< string, string > >( 'countries', [] );
const states = getSetting< Record< string, Record< string, string > > >(
'countryStates',
[]
);
const setLocationField =
( field: keyof PickupLocation ) => ( newValue: string | boolean ) => {
setValues( ( prevValue: PickupLocation ) => ( {
...prevValue,
[ field ]: newValue,
} ) );
};
const setLocationAddressField =
( field: keyof PickupLocation[ 'address' ] ) =>
( newValue: string | boolean ) => {
setValues( ( prevValue ) => ( {
...prevValue,
address: {
...prevValue.address,
[ field ]: newValue,
},
} ) );
};
return (
<form ref={ formRef }>
<TextControl
label={ __( 'Location Name', 'woo-gutenberg-products-block' ) }
name={ 'location_name' }
value={ values.name }
onChange={ setLocationField( 'name' ) }
autoComplete="off"
/>
<TextControl
label={ __( 'Address', 'woo-gutenberg-products-block' ) }
name={ 'location_address' }
placeholder={ __( 'Address', 'woo-gutenberg-products-block' ) }
value={ values.address.address_1 }
onChange={ setLocationAddressField( 'address_1' ) }
autoComplete="off"
/>
<TextControl
label={ __( 'City', 'woo-gutenberg-products-block' ) }
name={ 'location_city' }
hideLabelFromVision={ true }
placeholder={ __( 'City', 'woo-gutenberg-products-block' ) }
value={ values.address.city }
onChange={ setLocationAddressField( 'city' ) }
autoComplete="off"
/>
<TextControl
label={ __( 'Postcode / ZIP', 'woo-gutenberg-products-block' ) }
name={ 'location_postcode' }
hideLabelFromVision={ true }
placeholder={ __(
'Postcode / ZIP',
'woo-gutenberg-products-block'
) }
value={ values.address.postcode }
onChange={ setLocationAddressField( 'postcode' ) }
autoComplete="off"
/>
<StateControl
label={ __( 'State', 'woo-gutenberg-products-block' ) }
name={ 'location_state' }
hideLabelFromVision={ true }
placeholder={ __( 'State', 'woo-gutenberg-products-block' ) }
value={ values.address.state }
onChange={ setLocationAddressField( 'state' ) }
autoComplete="off"
states={ states }
currentCountry={ values.address.country }
/>
<SelectControl
label={ __( 'Country', 'woo-gutenberg-products-block' ) }
name={ 'location_country' }
hideLabelFromVision={ true }
placeholder={ __( 'Country', 'woo-gutenberg-products-block' ) }
value={ values.address.country }
onChange={ ( val: string ) => {
setLocationAddressField( 'state' )( '' );
setLocationAddressField( 'country' )( val );
} }
autoComplete="off"
options={ [
{
value: '',
disabled: true,
label: __( 'Country', 'woo-gutenberg-products-block' ),
},
...Object.entries( countries ).map(
( [ code, country ] ) => ( {
value: code,
label: country,
} )
),
] }
/>
<TextControl
label={ __( 'Pickup Details', 'woo-gutenberg-products-block' ) }
name={ 'pickup_details' }
value={ values.details }
onChange={ setLocationField( 'details' ) }
autoComplete="off"
/>
</form>
);
};
export default Form;

View File

@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { useRef, useState } from '@wordpress/element';
import type { UniqueIdentifier } from '@dnd-kit/core';
/**
* Internal dependencies
*/
import { SettingsModal } from '../../shared-components';
import Form from './form';
import type { PickupLocation } from '../types';
const EditLocation = ( {
locationData,
editingLocation,
onClose,
onSave,
onDelete,
}: {
locationData: PickupLocation | null;
editingLocation: UniqueIdentifier | 'new';
onClose: () => void;
onSave: ( location: PickupLocation ) => void;
onDelete: () => void;
} ): JSX.Element | null => {
const formRef = useRef( null );
const [ values, setValues ] = useState< PickupLocation >(
locationData as PickupLocation
);
if ( ! locationData ) {
return null;
}
return (
<SettingsModal
onRequestClose={ onClose }
title={
editingLocation === 'new'
? __( 'Pickup Location', 'woo-gutenberg-products-block' )
: __(
'Edit Pickup Location',
'woo-gutenberg-products-block'
)
}
actions={
<>
{ editingLocation !== 'new' && (
<Button
variant="link"
className="button-link-delete"
onClick={ () => {
onDelete();
onClose();
} }
>
{ __(
'Delete location',
'woo-gutenberg-products-block'
) }
</Button>
) }
<Button variant="secondary" onClick={ onClose }>
{ __( 'Cancel', 'woo-gutenberg-products-block' ) }
</Button>
<Button
variant="primary"
onClick={ () => {
onSave( values );
onClose();
} }
>
{ __( 'Done', 'woo-gutenberg-products-block' ) }
</Button>
</>
}
>
<Form
formRef={ formRef }
values={ values }
setValues={ setValues }
/>
</SettingsModal>
);
};
export default EditLocation;

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { SelectControl, TextControl } from '@wordpress/components';
const StateControl = ( {
states,
currentCountry,
...props
}: {
states: Record< string, Record< string, string > >;
currentCountry: string;
} ): JSX.Element | null => {
const filteredStates = states[ currentCountry ] || [];
if ( filteredStates.length === 0 ) {
return (
<TextControl
{ ...props }
disabled={ ! currentCountry || props.disabled }
/>
);
}
return (
<SelectControl
{ ...props }
options={ [
{
value: '',
disabled: true,
label: __( 'State', 'woo-gutenberg-products-block' ),
},
...Object.entries( filteredStates ).map(
( [ code, state ] ) => ( {
value: code,
label: state,
} )
),
] }
/>
);
};
export default StateControl;

View File

@ -0,0 +1,148 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ADMIN_URL } from '@woocommerce/settings';
import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
import {
CheckboxControl,
SelectControl,
TextControl,
ExternalLink,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { SettingsCard, SettingsSection } from '../shared-components';
import { useSettingsContext } from './settings-context';
const GeneralSettingsDescription = () => (
<>
<h2>{ __( 'General', 'woo-gutenberg-products-block' ) }</h2>
<p>
{ __(
'Enable or disable Local Pickup on your store, and define costs. Local Pickup is only available from the Block Checkout.',
'woo-gutenberg-products-block'
) }
</p>
<ExternalLink
href={ `${ ADMIN_URL }post.php?post=${ CHECKOUT_PAGE_ID }&action=edit` }
>
{ __( 'View checkout page', 'woo-gutenberg-products-block' ) }
</ExternalLink>
</>
);
const GeneralSettings = () => {
const { settings, setSettingField } = useSettingsContext();
const [ showCosts, setShowCosts ] = useState( !! settings.cost );
return (
<SettingsSection Description={ GeneralSettingsDescription }>
<SettingsCard>
<CheckboxControl
checked={ settings.enabled }
name="local_pickup_enabled"
onChange={ setSettingField( 'enabled' ) }
label={ __(
'Enable Local Pickup',
'woo-gutenberg-products-block'
) }
help={ __(
'When enabled, local pickup will appear as an option on the block based checkout.',
'woo-gutenberg-products-block'
) }
/>
<TextControl
label={ __( 'Title', 'woo-gutenberg-products-block' ) }
name="local_pickup_title"
help={ __(
'This is the shipping method title shown to customers.',
'woo-gutenberg-products-block'
) }
placeholder={ __(
'Local Pickup',
'woo-gutenberg-products-block'
) }
value={ settings.title }
onChange={ setSettingField( 'title' ) }
disabled={ false }
autoComplete="off"
/>
<CheckboxControl
checked={ showCosts }
onChange={ () => {
setShowCosts( ! showCosts );
setSettingField( 'cost' )( '' );
} }
label={ __(
'Add a price for customers who choose local pickup',
'woo-gutenberg-products-block'
) }
help={ __(
'By default, the local pickup shipping method is free.',
'woo-gutenberg-products-block'
) }
/>
{ showCosts ? (
<>
<TextControl
label={ __(
'Cost',
'woo-gutenberg-products-block'
) }
name="local_pickup_cost"
help={ __(
'Optional cost to charge for local pickup.',
'woo-gutenberg-products-block'
) }
placeholder={ __(
'Free',
'woo-gutenberg-products-block'
) }
type="number"
value={ settings.cost }
onChange={ setSettingField( 'cost' ) }
disabled={ false }
autoComplete="off"
/>
<SelectControl
label={ __(
'Taxes',
'woo-gutenberg-products-block'
) }
name="local_pickup_tax_status"
help={ __(
'If a cost is defined, this controls if taxes are applied to that cost.',
'woo-gutenberg-products-block'
) }
options={ [
{
label: __(
'Taxable',
'woo-gutenberg-products-block'
),
value: 'taxable',
},
{
label: __(
'Not taxable',
'woo-gutenberg-products-block'
),
value: 'none',
},
] }
value={ settings.tax_status }
onChange={ setSettingField( 'tax_status' ) }
disabled={ false }
/>
</>
) : null }
</SettingsCard>
</SettingsSection>
);
};
export default GeneralSettings;

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import SettingsPage from './settings-page';
const settingsContainer = document.getElementById(
'wc-shipping-method-pickup-location-settings-container'
);
if ( settingsContainer ) {
render( <SettingsPage />, settingsContainer );
}

View File

@ -0,0 +1,162 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import type { UniqueIdentifier } from '@dnd-kit/core';
import { isObject, isBoolean } from '@woocommerce/types';
import { ToggleControl, Button, ExternalLink } from '@wordpress/components';
import styled from '@emotion/styled';
/**
* Internal dependencies
*/
import {
SettingsSection,
SortableTable,
SortableData,
} from '../shared-components';
import EditLocation from './edit-location';
import type { SortablePickupLocation } from './types';
import { useSettingsContext } from './settings-context';
const LocationSettingsDescription = () => (
<>
<h2>{ __( 'Pickup Locations', 'woo-gutenberg-products-block' ) }</h2>
<p>
{ __(
'Define pickup locations for your customers to choose from during checkout.',
'woo-gutenberg-products-block'
) }
</p>
<ExternalLink href="https://woocommerce.com/document/local-pickup/">
{ __( 'Learn more', 'woo-gutenberg-products-block' ) }
</ExternalLink>
</>
);
const StyledAddress = styled.address`
color: #757575;
font-style: normal;
display: inline;
margin-left: 12px;
`;
const LocationSettings = () => {
const {
pickupLocations,
setPickupLocations,
toggleLocation,
updateLocation,
} = useSettingsContext();
const [ editingLocation, setEditingLocation ] =
useState< UniqueIdentifier >( '' );
const tableColumns = [
{
name: 'name',
label: __( 'Pickup Location', 'woo-gutenberg-products-block' ),
width: '50%',
renderCallback: ( row: SortableData ): JSX.Element => (
<>
{ row.name }
<StyledAddress>
{ isObject( row.address ) &&
Object.values( row.address )
.filter( ( value ) => value !== '' )
.join( ', ' ) }
</StyledAddress>
</>
),
},
{
name: 'enabled',
label: __( 'Enabled', 'woo-gutenberg-products-block' ),
align: 'right',
renderCallback: ( row: SortableData ): JSX.Element => (
<ToggleControl
checked={ isBoolean( row.enabled ) ? row.enabled : false }
onChange={ () => toggleLocation( row.id ) }
/>
),
},
{
name: 'edit',
label: '',
align: 'center',
width: '1%',
renderCallback: ( row: SortableData ): JSX.Element => (
<button
type="button"
className="button-link-edit button-link"
onClick={ () => {
setEditingLocation( row.id );
} }
>
{ __( 'Edit', 'woo-gutenberg-products-block' ) }
</button>
),
},
];
const FooterContent = (): JSX.Element => (
<Button
variant="secondary"
onClick={ () => {
setEditingLocation( 'new' );
} }
>
{ __( 'Add Pickup Location', 'woo-gutenberg-products-block' ) }
</Button>
);
return (
<SettingsSection Description={ LocationSettingsDescription }>
<SortableTable
className="pickup-locations"
columns={ tableColumns }
data={ pickupLocations }
setData={ ( newData ) => {
setPickupLocations( newData as SortablePickupLocation[] );
} }
footerContent={ FooterContent }
/>
{ editingLocation && (
<EditLocation
locationData={
editingLocation === 'new'
? {
name: '',
details: '',
enabled: true,
address: {
address_1: '',
city: '',
state: '',
postcode: '',
country: '',
},
}
: pickupLocations.find( ( { id } ) => {
return id === editingLocation;
} ) || null
}
editingLocation={ editingLocation }
onSave={ ( values ) => {
updateLocation(
editingLocation,
values as SortablePickupLocation
);
} }
onClose={ () => setEditingLocation( '' ) }
onDelete={ () => {
updateLocation( editingLocation, null );
setEditingLocation( '' );
} }
/>
) }
</SettingsSection>
);
};
export default LocationSettings;

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import styled from '@emotion/styled';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import { SettingsSection } from '../shared-components';
import { useSettingsContext } from './settings-context';
const SaveSectionWrapper = styled( SettingsSection )`
text-align: right;
padding-top: 0;
margin-top: 0;
`;
const SaveSettings = () => {
const { isSaving, save } = useSettingsContext();
return (
<SaveSectionWrapper className={ 'submit' }>
<Button
variant="primary"
isBusy={ isSaving }
disabled={ isSaving }
onClick={ save }
>
{ __( 'Save changes', 'woo-gutenberg-products-block' ) }
</Button>
</SaveSectionWrapper>
);
};
export default SaveSettings;

View File

@ -0,0 +1,171 @@
/**
* External dependencies
*/
import {
createContext,
useContext,
useCallback,
useState,
} from '@wordpress/element';
import { cleanForSlug } from '@wordpress/url';
import type { UniqueIdentifier } from '@dnd-kit/core';
import apiFetch from '@wordpress/api-fetch';
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { isEqual } from 'lodash';
/**
* Internal dependencies
*/
import type {
SortablePickupLocation,
SettingsContextType,
ShippingMethodSettings,
} from './types';
import {
defaultSettings,
getInitialSettings,
getInitialPickupLocations,
} from './utils';
const SettingsContext = createContext< SettingsContextType >( {
settings: defaultSettings,
setSettingField: () => () => void null,
pickupLocations: [],
setPickupLocations: () => void null,
toggleLocation: () => void null,
updateLocation: () => void null,
isSaving: false,
save: () => void null,
} );
export const useSettingsContext = (): SettingsContextType => {
return useContext( SettingsContext );
};
export const SettingsProvider = ( {
children,
}: {
children: JSX.Element[] | JSX.Element;
} ): JSX.Element => {
const [ isSaving, setIsSaving ] = useState( false );
const [ pickupLocations, setPickupLocations ] = useState<
SortablePickupLocation[]
>( getInitialPickupLocations );
const [ settings, setSettings ] =
useState< ShippingMethodSettings >( getInitialSettings );
const setSettingField = useCallback(
( field: keyof ShippingMethodSettings ) => ( newValue: unknown ) => {
setSettings( ( prevValue ) => ( {
...prevValue,
[ field ]: newValue,
} ) );
},
[]
);
const toggleLocation = useCallback( ( rowId: UniqueIdentifier ) => {
setPickupLocations( ( previousLocations: SortablePickupLocation[] ) => {
const locationIndex = previousLocations.findIndex(
( { id } ) => id === rowId
);
const updated = [ ...previousLocations ];
updated[ locationIndex ].enabled =
! previousLocations[ locationIndex ].enabled;
return updated;
} );
}, [] );
const updateLocation = (
rowId: UniqueIdentifier | 'new',
locationData: SortablePickupLocation
) => {
setPickupLocations( ( prevData ) => {
if ( rowId === 'new' ) {
return [
...prevData,
{
...locationData,
id:
cleanForSlug( locationData.name ) +
'-' +
prevData.length,
},
];
}
return prevData
.map( ( location ): SortablePickupLocation => {
if ( location.id === rowId ) {
return locationData;
}
return location;
} )
.filter( Boolean );
} );
};
const save = useCallback( () => {
const data = {
pickup_location_settings: {
enabled: settings.enabled ? 'yes' : 'no',
title: settings.title,
tax_status: [ 'taxable', 'none' ].includes(
settings.tax_status
)
? settings.tax_status
: 'taxable',
cost: settings.cost,
},
pickup_locations: pickupLocations.map( ( location ) => ( {
name: location.name,
address: location.address,
details: location.details,
enabled: location.enabled,
} ) ),
};
setIsSaving( true );
// @todo This should be improved to include error handling in case of API failure, or invalid data being sent that
// does not match the schema. This would fail silently on the API side.
apiFetch( {
path: '/wp/v2/settings',
method: 'POST',
data,
} ).then( ( response ) => {
setIsSaving( false );
if (
isEqual(
response.pickup_location_settings,
data.pickup_location_settings
) &&
isEqual( response.pickup_locations, data.pickup_locations )
) {
dispatch( 'core/notices' ).createSuccessNotice(
__(
'Local Pickup settings have been saved.',
'woo-gutenberg-products-block'
)
);
}
} );
}, [ settings, pickupLocations ] );
const settingsData = {
settings,
setSettingField,
pickupLocations,
setPickupLocations,
toggleLocation,
updateLocation,
isSaving,
save,
};
return (
<SettingsContext.Provider value={ settingsData }>
{ children }
</SettingsContext.Provider>
);
};

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';
/**
* Internal dependencies
*/
import GeneralSettings from './general-settings';
import LocationSettings from './location-settings';
import SaveSettings from './save';
import { SettingsProvider } from './settings-context';
const SettingsWrapper = styled.div`
margin: 48px auto 0;
max-width: 1032px;
display: flex;
flex-flow: column;
@media ( min-width: 960px ) {
padding: 0 56px;
}
`;
const SettingsPage = () => {
return (
<SettingsWrapper id="local-pickup-settings">
<SettingsProvider>
<GeneralSettings />
<LocationSettings />
<SaveSettings />
</SettingsProvider>
</SettingsWrapper>
);
};
export default SettingsPage;

View File

@ -0,0 +1,47 @@
/**
* External dependencies
*/
import type { UniqueIdentifier } from '@dnd-kit/core';
/**
* Internal dependencies
*/
import type { SortableData } from '../shared-components';
export interface PickupLocation {
name: string;
details: string;
enabled: boolean;
address: {
address_1: string;
city: string;
state: string;
postcode: string;
country: string;
};
}
export interface SortablePickupLocation extends PickupLocation, SortableData {}
export type ShippingMethodSettings = {
enabled: boolean;
title: string;
tax_status: string;
cost: string;
};
export type SettingsContextType = {
settings: ShippingMethodSettings;
setSettingField: (
field: keyof ShippingMethodSettings
) => ( value: unknown ) => void;
pickupLocations: SortablePickupLocation[];
setPickupLocations: ( locations: SortablePickupLocation[] ) => void;
toggleLocation: ( rowId: UniqueIdentifier ) => void;
updateLocation: (
rowId: UniqueIdentifier | 'new',
location: SortablePickupLocation | null
) => void;
isSaving: boolean;
save: () => void;
};

View File

@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { cleanForSlug } from '@wordpress/url';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import type {
PickupLocation,
SortablePickupLocation,
ShippingMethodSettings,
} from './types';
export const indexLocationsById = (
locations: PickupLocation[]
): SortablePickupLocation[] => {
return locations.map( ( value, index ) => {
return {
...value,
id: cleanForSlug( value.name ) + '-' + index,
};
} );
};
export const defaultSettings = {
enabled: 'yes',
title: '',
tax_status: 'taxable',
cost: '',
};
export const getInitialSettings = (): ShippingMethodSettings => {
const settings = getSetting(
'pickupLocationSettings',
defaultSettings
) as typeof defaultSettings;
return {
enabled: settings?.enabled === 'yes',
title: settings?.title || defaultSettings.title,
tax_status: settings?.tax_status || defaultSettings.tax_status,
cost: settings?.cost || defaultSettings.cost,
};
};
export const getInitialPickupLocations = (): SortablePickupLocation[] =>
indexLocationsById( getSetting( 'pickupLocations', [] ) );

View File

@ -0,0 +1,4 @@
export { default as SettingsCard } from './settings-card';
export { default as SettingsSection } from './settings-section';
export { default as SettingsModal } from './settings-modal';
export * from './sortable-table';

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { Card, CardBody } from '@wordpress/components';
import styled from '@emotion/styled';
const StyledCard = styled( Card )`
border-radius: 3px;
`;
const StyledCardBody = styled( CardBody )`
// increasing the specificity of the styles to override the Gutenberg ones
&.is-size-medium.is-size-medium {
padding: 24px;
}
h4 {
margin-top: 0;
margin-bottom: 1em;
}
> * {
margin-top: 0;
margin-bottom: 1em;
// fixing the spacing on the inputs and their help text, to ensure it is consistent
&:last-child {
margin-bottom: 0;
> :last-child {
margin-bottom: 0;
}
}
}
input,
select {
margin: 0;
}
// spacing adjustment on "Express checkouts > Show express checkouts on" list
ul > li:last-child {
margin-bottom: 0;
.components-base-control__field {
margin-bottom: 0;
}
}
`;
const SettingsCard = ( {
children,
...props
}: {
children: ( JSX.Element | null )[];
} ): JSX.Element => (
<StyledCard>
<StyledCardBody { ...props }>{ children }</StyledCardBody>
</StyledCard>
);
export default SettingsCard;

View File

@ -0,0 +1,89 @@
/**
* External dependencies
*/
import { Modal } from '@wordpress/components';
import { HorizontalRule } from '@wordpress/primitives';
import styled from '@emotion/styled';
const StyledModal = styled( Modal )`
max-width: 600px;
border-radius: 4px;
@media ( min-width: 600px ) {
min-width: 560px;
}
.components-modal__header {
padding: 12px 24px;
border-bottom: 1px solid #e0e0e0;
position: relative;
height: auto;
width: auto;
margin: 0 -24px 16px;
@media ( max-width: 599px ) {
button {
display: none;
}
}
}
.components-modal__content {
margin: 0;
padding: 0 24px;
@media ( max-width: 599px ) {
display: flex;
flex-direction: column;
hr:last-of-type {
margin-top: auto;
}
}
.components-base-control {
label {
margin-top: 8px;
text-transform: capitalize !important;
}
}
}
`;
const StyledFooter = styled.div`
display: flex;
justify-content: flex-end;
border-top: 1px solid #e0e0e0;
margin: 24px -24px 0;
padding: 24px;
> * {
&:not( :first-of-type ) {
margin-left: 8px;
}
}
.button-link-delete {
margin-right: auto;
color: #d63638;
}
`;
const SettingsModal = ( {
children,
actions,
title,
onRequestClose,
...props
}: {
children: React.ReactNode;
actions: React.ReactNode;
title: string;
onRequestClose: () => void;
} ): JSX.Element => (
<StyledModal title={ title } onRequestClose={ onRequestClose } { ...props }>
{ children }
<StyledFooter>{ actions }</StyledFooter>
</StyledModal>
);
export default SettingsModal;

View File

@ -0,0 +1,67 @@
/**
* External dependencies
*/
import React from 'react';
import styled from '@emotion/styled';
const StyledSectionWrapper = styled.div`
display: flex;
flex-flow: column;
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
@media ( min-width: 800px ) {
flex-flow: row;
}
.components-base-control {
label {
text-transform: capitalize !important;
}
}
`;
const StyledDescriptionWrapper = styled.div`
flex: 0 1 auto;
margin-bottom: 24px;
@media ( min-width: 800px ) {
flex: 0 0 250px;
margin: 0 32px 0 0;
}
h2 {
font-size: 16px;
line-height: 24px;
}
p {
font-size: 13px;
line-height: 17.89px;
margin: 12px 0;
}
> :last-child {
margin-bottom: 0;
}
`;
const StyledSectionControls = styled.div`
flex: 1 1 auto;
margin-bottom: 12px;
`;
const SettingsSection = ( {
Description = () => null,
children,
...props
}: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Description?: () => JSX.Element | null;
children: React.ReactNode;
} ): JSX.Element => (
<StyledSectionWrapper { ...props }>
<StyledDescriptionWrapper>
<Description />
</StyledDescriptionWrapper>
<StyledSectionControls>{ children }</StyledSectionControls>
</StyledSectionWrapper>
);
export default SettingsSection;

View File

@ -0,0 +1,296 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';
import { Icon, dragHandle } from '@wordpress/icons';
import { useMemo } from '@wordpress/element';
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
DragEndEvent,
UniqueIdentifier,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
arrayMove,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { objectHasProp } from '@woocommerce/types';
export interface SortableData extends Record< string, unknown > {
id: UniqueIdentifier;
}
type ColumnProps = {
name: string;
label: string;
width?: string;
align?: string;
renderCallback?: ( row: SortableData ) => JSX.Element;
};
const TableRow = ( {
children,
id,
}: {
children: JSX.Element[];
id: UniqueIdentifier;
} ): JSX.Element => {
const { attributes, listeners, transform, transition, setNodeRef } =
useSortable( {
id,
} );
const style = {
transform: CSS.Transform.toString( transform ),
transition,
};
return (
<tr ref={ setNodeRef } style={ style }>
<>
<td>
<Icon
icon={ dragHandle }
size={ 14 }
className={ 'sortable-table__handle' }
{ ...attributes }
{ ...listeners }
/>
</td>
{ children }
</>
</tr>
);
};
const StyledTable = styled.table`
background: #fff;
border: 0;
border-radius: 3px;
box-shadow: 0 0 0 1px rgb( 0 0 0 / 10% );
border-spacing: 0;
width: 100%;
clear: both;
margin: 0;
font-size: 14px;
.align-left {
text-align: left;
.components-flex {
justify-content: flex-start;
gap: 0;
}
}
.align-right {
text-align: right;
.components-flex {
justify-content: flex-end;
gap: 0;
}
}
.align-center {
text-align: center;
> * {
margin: 0 auto;
}
.components-flex {
display: block;
}
}
.sortable-table__handle {
cursor: move;
}
th {
position: relative;
color: #2c3338;
text-align: left;
vertical-align: middle;
vertical-align: top;
word-wrap: break-word;
}
tbody {
td {
vertical-align: top;
margin-bottom: 9px;
}
}
tfoot {
td {
text-align: left;
vertical-align: middle;
}
}
thead,
tfoot,
tbody {
td,
th {
border-top: 1px solid rgb( 0 0 0 / 10% );
border-bottom: 1px solid rgb( 0 0 0 / 10% );
padding: 16px 0 16px 24px;
line-height: 1.5;
&:last-child {
padding-right: 24px;
}
> svg,
> .components-base-control {
margin: 3px 0;
}
}
}
thead th {
border-top: 0;
}
tfoot td {
border-bottom: 0;
}
`;
export const SortableTable = ( {
columns,
data,
setData,
className,
footerContent: FooterContent,
}: {
columns: ColumnProps[];
data: SortableData[];
setData: ( data: SortableData[] ) => void;
className?: string;
footerContent?: () => JSX.Element;
} ): JSX.Element => {
const items = useMemo( () => data.map( ( { id } ) => id ), [ data ] );
const sensors = useSensors(
useSensor( MouseSensor, {} ),
useSensor( TouchSensor, {} ),
useSensor( KeyboardSensor, {} )
);
function handleDragEnd( event: DragEndEvent ) {
const { active, over } = event;
if ( active !== null && over !== null && active?.id !== over?.id ) {
const newData = arrayMove(
data,
items.indexOf( active.id ),
items.indexOf( over.id )
);
setData( newData );
}
}
const getColumnProps = ( column: ColumnProps, parentClassName: string ) => {
const align = column?.align || 'left';
const width = column?.width || 'auto';
return {
className: `${ parentClassName }-${ column.name } align-${ align }`,
style: { width },
};
};
return (
<DndContext
sensors={ sensors }
onDragEnd={ handleDragEnd }
collisionDetection={ closestCenter }
modifiers={ [ restrictToVerticalAxis ] }
>
<StyledTable className={ `${ className } sortable-table` }>
<thead>
<tr>
<th
className={ `sortable-table__sort` }
style={ { width: '1%' } }
>
&nbsp;
</th>
{ columns.map( ( column ) => (
<th
key={ column.name }
{ ...getColumnProps(
column,
`sortable-table__column`
) }
>
{ column.label }
</th>
) ) }
</tr>
</thead>
{ FooterContent && (
<tfoot>
<tr>
<td colSpan={ columns.length + 1 }>
<FooterContent />
</td>
</tr>
</tfoot>
) }
<tbody>
<SortableContext
items={ items }
strategy={ verticalListSortingStrategy }
>
{ data &&
data.map(
( row ) =>
row && (
<TableRow
key={ row.id }
id={ row.id }
className={ className }
>
{ columns.map( ( column ) => (
<td
key={ `${ row.id }-${ column.name }` }
{ ...getColumnProps(
column,
`sortable-table__column`
) }
>
{ column.renderCallback ? (
column.renderCallback(
row
)
) : (
<>
{ objectHasProp(
row,
column.name
) &&
row[
column.name
] }
</>
) }
</td>
) ) }
</TableRow>
)
) }
</SortableContext>
</tbody>
</StyledTable>
</DndContext>
);
};
export default SortableTable;

View File

@ -564,6 +564,12 @@ const getExtensionsConfig = ( options = {} ) => {
},
},
},
{
test: /\.s[c|a]ss$/,
use: {
loader: 'ignore-loader',
},
},
],
},
optimization: {

View File

@ -142,17 +142,19 @@ const entries = {
},
payments: {
'wc-payment-method-cheque':
'./assets/js/payment-method-extensions/payment-methods/cheque/index.js',
'./assets/js/extensions/payment-methods/cheque/index.js',
'wc-payment-method-paypal':
'./assets/js/payment-method-extensions/payment-methods/paypal/index.js',
'./assets/js/extensions/payment-methods/paypal/index.js',
'wc-payment-method-bacs':
'./assets/js/payment-method-extensions/payment-methods/bacs/index.js',
'./assets/js/extensions/payment-methods/bacs/index.js',
'wc-payment-method-cod':
'./assets/js/payment-method-extensions/payment-methods/cod/index.js',
'./assets/js/extensions/payment-methods/cod/index.js',
},
extensions: {
'wc-blocks-google-analytics':
'./assets/js/extensions/google-analytics/index.ts',
'wc-shipping-method-pickup-location':
'./assets/js/extensions/shipping-methods/pickup-location/index.js',
},
};

View File

@ -10,6 +10,11 @@
"hasInstallScript": true,
"license": "GPL-3.0+",
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/modifiers": "^6.0.0",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@emotion/styled": "^11.10.5",
"@wordpress/autop": "3.16.0",
"@wordpress/compose": "5.5.0",
"@wordpress/deprecated": "3.16.0",
@ -2290,6 +2295,67 @@
"node": ">=10.0.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.5.tgz",
"integrity": "sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.0.tgz",
"integrity": "sha512-V3+JSo6/BTcgPRHiNUTSKgqVv/doKXg+T4Z0QvKiiXp+uIyJTUtPkQOBRQApUWi3ApBhnoWljyt/3xxY4fTd0Q==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.1.tgz",
"integrity": "sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.4",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.0.tgz",
"integrity": "sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.9.2",
"license": "MIT",
@ -2432,11 +2498,12 @@
"version": "11.9.3",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@emotion/babel-plugin": "^11.7.1",
"@emotion/is-prop-valid": "^1.1.3",
"@emotion/serialize": "^1.0.4",
"@emotion/utils": "^1.1.0"
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.10.5",
"@emotion/is-prop-valid": "^1.2.0",
"@emotion/serialize": "^1.1.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
"@emotion/utils": "^1.2.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0",
@ -2456,9 +2523,69 @@
"version": "1.1.3",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.7.4"
"regenerator-runtime": "^0.13.10"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/styled/node_modules/@emotion/babel-plugin": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz",
"integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/plugin-syntax-jsx": "^7.17.12",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/serialize": "^1.1.1",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.1.3"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@emotion/styled/node_modules/@emotion/hash": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
"integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
},
"node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
"integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
"dependencies": {
"@emotion/memoize": "^0.8.0"
}
},
"node_modules/@emotion/styled/node_modules/@emotion/memoize": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
"integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
},
"node_modules/@emotion/styled/node_modules/@emotion/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
"dependencies": {
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/unitless": "^0.8.0",
"@emotion/utils": "^1.2.0",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/styled/node_modules/@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"node_modules/@emotion/styled/node_modules/@emotion/utils": {
"version": "1.1.0",
"license": "MIT"
@ -2467,6 +2594,14 @@
"version": "0.7.5",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
"integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.0.0",
"license": "MIT"
@ -51719,6 +51854,50 @@
"version": "0.5.7",
"dev": true
},
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
}
},
"@dnd-kit/core": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.5.tgz",
"integrity": "sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/modifiers": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.0.tgz",
"integrity": "sha512-V3+JSo6/BTcgPRHiNUTSKgqVv/doKXg+T4Z0QvKiiXp+uIyJTUtPkQOBRQApUWi3ApBhnoWljyt/3xxY4fTd0Q==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/sortable": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.1.tgz",
"integrity": "sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/utilities": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.0.tgz",
"integrity": "sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==",
"requires": {
"tslib": "^2.0.0"
}
},
"@emotion/babel-plugin": {
"version": "11.9.2",
"requires": {
@ -51825,19 +52004,74 @@
"@emotion/styled": {
"version": "11.9.3",
"requires": {
"@babel/runtime": "^7.13.10",
"@emotion/babel-plugin": "^11.7.1",
"@emotion/is-prop-valid": "^1.1.3",
"@emotion/serialize": "^1.0.4",
"@emotion/utils": "^1.1.0"
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.10.5",
"@emotion/is-prop-valid": "^1.2.0",
"@emotion/serialize": "^1.1.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
"@emotion/utils": "^1.2.0"
},
"dependencies": {
"@emotion/is-prop-valid": {
"version": "1.1.3",
"requires": {
"@emotion/memoize": "^0.7.4"
"regenerator-runtime": "^0.13.10"
}
},
"@emotion/babel-plugin": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz",
"integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==",
"requires": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/plugin-syntax-jsx": "^7.17.12",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/serialize": "^1.1.1",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.1.3"
}
},
"@emotion/hash": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
"integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
},
"@emotion/is-prop-valid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
"integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
"requires": {
"@emotion/memoize": "^0.8.0"
}
},
"@emotion/memoize": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
"integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
},
"@emotion/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
"requires": {
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/unitless": "^0.8.0",
"@emotion/utils": "^1.2.0",
"csstype": "^3.0.2"
}
},
"@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"@emotion/utils": {
"version": "1.1.0"
}
@ -51846,6 +52080,12 @@
"@emotion/unitless": {
"version": "0.7.5"
},
"@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
"integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
"requires": {}
},
"@emotion/utils": {
"version": "1.0.0"
},

View File

@ -225,6 +225,11 @@
"npm": "^8.0.0"
},
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/modifiers": "^6.0.0",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@emotion/styled": "^11.10.5",
"@wordpress/autop": "3.16.0",
"@wordpress/compose": "5.5.0",
"@wordpress/deprecated": "3.16.0",

View File

@ -349,8 +349,10 @@ class Bootstrap {
);
$this->container->register(
ShippingController::class,
function () {
return new ShippingController( $this->package );
function ( $container ) {
$asset_api = $container->get( AssetApi::class );
$asset_data_registry = $container->get( AssetDataRegistry::class );
return new ShippingController( $asset_api, $asset_data_registry );
}
);
}

View File

@ -2,8 +2,6 @@
namespace Automattic\WooCommerce\Blocks\Shipping;
use WC_Shipping_Method;
use WC_Order;
use Automattic\WooCommerce\Utilities\ArrayUtil;
/**
* Local Pickup Shipping Method.
@ -24,16 +22,11 @@ class PickupLocation extends WC_Shipping_Method {
* Init function.
*/
public function init() {
$this->init_form_fields();
$this->init_settings();
$this->enabled = $this->get_option( 'enabled' );
$this->title = $this->get_option( 'title' );
$this->tax_status = $this->get_option( 'tax_status' );
$this->cost = $this->get_option( 'cost' );
$this->pickup_locations = get_option( $this->id . '_pickup_locations', [] );
add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
add_filter( 'woocommerce_attribute_label', array( $this, 'translate_meta_data' ), 10, 3 );
}
@ -66,45 +59,6 @@ class PickupLocation extends WC_Shipping_Method {
}
}
/**
* Initialize form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable', 'woo-gutenberg-products-block' ),
'type' => 'checkbox',
'label' => __( 'If enabled, this method will appear on the block based checkout.', 'woo-gutenberg-products-block' ),
'default' => 'no',
),
'title' => array(
'title' => __( 'Title', 'woo-gutenberg-products-block' ),
'type' => 'text',
'description' => __( 'This controls the title which the user sees during checkout.', 'woo-gutenberg-products-block' ),
'default' => __( 'Local pickup', 'woo-gutenberg-products-block' ),
'desc_tip' => true,
),
'tax_status' => array(
'title' => __( 'Tax status', 'woo-gutenberg-products-block' ),
'type' => 'select',
'class' => 'wc-enhanced-select',
'default' => 'taxable',
'options' => array(
'taxable' => __( 'Taxable', 'woo-gutenberg-products-block' ),
'none' => _x( 'None', 'Tax status', 'woo-gutenberg-products-block' ),
),
),
'cost' => array(
'title' => __( 'Cost', 'woo-gutenberg-products-block' ),
'type' => 'text',
'placeholder' => '0',
'description' => __( 'Optional cost for local pickup.', 'woo-gutenberg-products-block' ),
'default' => '',
'desc_tip' => true,
),
);
}
/**
* See if the method is available.
*
@ -112,37 +66,10 @@ class PickupLocation extends WC_Shipping_Method {
* @return bool
*/
public function is_available( $package ) {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', 'yes' === $this->enabled, $package, $this );
}
/**
* Process options in admin.
*/
public function process_admin_options() {
parent::process_admin_options();
$locations = [];
$location_names = array_map( 'sanitize_text_field', wp_unslash( $_POST['locationName'] ?? [] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
foreach ( $location_names as $index => $location_name ) {
$locations[] = [
'name' => $location_name,
'address' => [
'address_1' => wc_clean( wp_unslash( $_POST['address_1'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'city' => wc_clean( wp_unslash( $_POST['city'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'state' => wc_clean( wp_unslash( $_POST['state'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'postcode' => wc_clean( wp_unslash( $_POST['postcode'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'country' => wc_clean( wp_unslash( $_POST['country'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
],
'details' => wc_clean( wp_unslash( $_POST['details'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'enabled' => wc_string_to_bool( wc_clean( wp_unslash( $_POST['locationEnabled'][ $index ] ?? 1 ) ) ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification.Missing
];
}
update_option( $this->id . '_pickup_locations', $locations );
$this->pickup_locations = $locations;
}
/**
* Translates meta data for the shipping method.
*
@ -170,315 +97,12 @@ class PickupLocation extends WC_Shipping_Method {
* See also WC_Shipping_Method::admin_options().
*/
public function admin_options() {
parent::admin_options();
?>
<table class="form-table" id="pickup_locations">
<tbody>
<tr valign="top" class="">
<th scope="row" class="titledesc">
<label>
<?php esc_html_e( 'Pickup Locations', 'woo-gutenberg-products-block' ); ?>
</label>
</th>
<td class="">
<table class="wc-local-pickup-locations wc_shipping widefat sortable">
<thead>
<tr>
<th class="wc-local-pickup-location-sort"><?php echo wc_help_tip( __( 'Drag and drop to re-order your pickup locations.', 'woo-gutenberg-products-block' ) ); ?></th>
<th class="wc-local-pickup-location-name"><?php esc_html_e( 'Location Name', 'woo-gutenberg-products-block' ); ?></th>
<th class="wc-local-pickup-location-enabled"><?php esc_html_e( 'Enabled', 'woo-gutenberg-products-block' ); ?></th>
<th class="wc-local-pickup-location-address"><?php esc_html_e( 'Pickup Address', 'woo-gutenberg-products-block' ); ?></th>
<th class="wc-local-pickup-location-details"><?php esc_html_e( 'Pickup Details', 'woo-gutenberg-products-block' ); ?></th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="5">
<button type="button" class="button-add-location button"><?php esc_html_e( 'Add pickup location', 'woo-gutenberg-products-block' ); ?></button>
</th>
</tr>
</tfoot>
<tbody>
<?php
foreach ( $this->pickup_locations as $location ) {
echo '<tr>';
echo $this->pickup_location_row( $location ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</tr>';
}
?>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<style>
.wc-local-pickup-locations thead th {
vertical-align: middle;
}
.wc-local-pickup-locations tbody td {
border-top: 2px solid #f9f9f9;
}
.wc-local-pickup-locations tbody tr:nth-child( odd ) td {
background: #f9f9f9;
}
td.wc-local-pickup-location-sort {
cursor: move;
font-size: 15px;
text-align: center;
}
td.wc-local-pickup-location-sort::before {
content: '\f333';
font-family: 'Dashicons';
text-align: center;
line-height: 1;
color: #999;
display: block;
width: 17px;
float: left;
height: 100%;
line-height: 24px;
}
td.wc-local-pickup-location-sort:hover::before {
color: #333;
}
.wc-local-pickup-location-enabled {
text-align: center;
}
.wc-local-pickup-locations .wc-local-pickup-location-name,
.wc-local-pickup-locations .wc-local-pickup-location-address,
.wc-local-pickup-locations .wc-local-pickup-location-details {
width: 25%;
padding-top: 10px !important;
padding-bottom: 10px !important;
}
global $hide_save_button;
$hide_save_button = true;
#pickup_locations .wc-local-pickup-locations .editing .view {
display: none;
}
#pickup_locations .wc-local-pickup-locations .editing .edit {
display: block;
}
#pickup_locations .wc-local-pickup-locations .editable input,
#pickup_locations .wc-local-pickup-locations .editable textarea,
#pickup_locations .wc-local-pickup-locations .editable select {
vertical-align: middle;
text-overflow: ellipsis;
width: 100%;
margin: 2px 0;
}
#pickup_locations .wc-local-pickup-locations .editable textarea {
padding: 5px;
}
wp_enqueue_script( 'wc-shipping-method-pickup-location' );
#pickup_locations .wc-local-pickup-locations tr .row-actions {
position: relative;
}
#pickup_locations .wc-local-pickup-locations tr:hover .row-actions {
position: static;
}
</style>
<script type="text/javascript">
const states = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( WC()->countries->get_states() ) ); ?>' ) );
const locationsTable = document.querySelectorAll("table.wc-local-pickup-locations")[0];
function insertRow() {
var tbodyRef = document.querySelectorAll('#pickup_locations table tbody')[0];
let size = tbodyRef.getElementsByTagName('tr').length;
let newRow = tbodyRef.insertRow( size );
var txt = document.createElement('textarea');
txt.innerHTML = '<?php echo esc_js( $this->pickup_location_row() ); ?>';
newRow.innerHTML = txt.value;
}
locationsTable.addEventListener( "click", function(event) {
const addButton = event.target.closest( "button.button-add-location" );
const editButton = event.target.closest('button.button-link-edit');
const deleteButton = event.target.closest('button.button-link-delete');
const enabledToggleButton = event.target.closest('button.enabled-toggle-button');
if (addButton !== null) {
insertRow();
return false;
}
if (editButton !== null) {
const toggleText = editButton.dataset.toggle;
const innerText = editButton.innerText;
const tr = editButton.parentElement.parentElement.parentElement;
if ( tr.classList.contains('editing') ) {
tr.querySelectorAll('.edit').forEach( function( element ) {
const newValues = [];
element.querySelectorAll('.edit input[name], .edit textarea[name], .edit select[name]').forEach( function( input ) {
const newValue = input.value;
if ( newValue ) {
newValues.push( input.value );
}
});
element.parentElement.querySelector('.view').innerText = newValues.join(', ') || '—';
});
}
editButton.innerText = toggleText;
editButton.dataset.toggle = innerText;
editButton.parentElement.parentElement.parentElement.classList.toggle('editing');
return false;
}
if (deleteButton !== null) {
deleteButton.parentElement.parentElement.parentElement.remove();
return false;
}
if (enabledToggleButton !== null) {
var toggleDisplay = enabledToggleButton.parentElement.querySelectorAll(".woocommerce-input-toggle")[0];
var toggleInput = enabledToggleButton.parentElement.querySelectorAll("input")[0];
if ( toggleDisplay.classList.contains("woocommerce-input-toggle--enabled") ) {
toggleInput.value = 0;
toggleDisplay.classList.add("woocommerce-input-toggle--disabled");
toggleDisplay.classList.remove("woocommerce-input-toggle--enabled");
toggleDisplay.textContent = "<?php echo esc_js( 'No', 'woo-gutenberg-products-block' ); ?>";
} else {
toggleInput.value = 1;
toggleDisplay.classList.remove("woocommerce-input-toggle--disabled");
toggleDisplay.classList.add("woocommerce-input-toggle--enabled");
toggleDisplay.textContent = "<?php echo esc_js( 'Yes', 'woo-gutenberg-products-block' ); ?>";
}
return false;
}
return true;
}, false );
function initCountrySelect() {
var event = new Event('change');
locationsTable.querySelectorAll('select.country-select').forEach(function(countrySelect) {
countrySelect.dispatchEvent(event);
});
}
function changeCountry( countrySelect ) {
const stateInput = countrySelect.parentElement.querySelectorAll('input.state-input')[0];
const stateSelect = countrySelect.parentElement.querySelectorAll('select.state-select')[0];
const selectedCountry = countrySelect.value;
const selectedState = stateInput.value;
if ( selectedCountry === "" ) {
countrySelect.classList.add("placeholder");
} else {
countrySelect.classList.remove("placeholder");
}
if (states[selectedCountry] === undefined || states[selectedCountry].length === 0) {
stateSelect.hidden = true;
stateInput.type = 'text';
return;
}
stateSelect.innerHTML = '';
for (const [key, value] of Object.entries(states[selectedCountry])) {
const option = document.createElement("option");
option.value = key;
option.text = value;
option.selected = selectedState === key;
stateSelect.add( option );
};
stateSelect.hidden = false;
stateInput.type = 'hidden';
}
function changeState( stateSelect ) {
const stateInput = stateSelect.parentElement.querySelectorAll('input.state-input')[0];
stateInput.value = stateSelect.value;
}
locationsTable.addEventListener( "change", function(event) {
const countrySelect = event.target.closest('select.country-select');
const stateSelect = event.target.closest('select.state-select');
if (countrySelect !== null) {
changeCountry( countrySelect );
}
if (stateSelect !== null) {
changeState( stateSelect );
}
}, true );
initCountrySelect();
</script>
<?php
}
/**
* Row for the settings table.
*
* @param array $location Location data.
* @return string
*/
protected function pickup_location_row( $location = [] ) {
ob_start();
$location = wp_parse_args(
$location,
[
'name' => '',
'enabled' => false,
'details' => '',
]
);
$location['address'] = wp_parse_args(
$location['address'] ?? [],
[
'address_1' => '',
'city' => '',
'state' => '',
'postcode' => '',
'country' => '',
]
);
?>
<td width="1%" class="wc-local-pickup-location-sort sort"></td>
<td class="wc-local-pickup-location-name editable">
<div class="view"><?php echo esc_html( $location['name'] ?? '' ); ?></div>
<div class="edit" hidden><input type="text" name="locationName[]" value="<?php echo esc_attr( $location['name'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'New location', 'woo-gutenberg-products-block' ); ?>" /></div>
<div class="row-actions">
<button type="button" class="button-link-edit button-link" data-toggle="<?php esc_attr_e( 'Done', 'woo-gutenberg-products-block' ); ?>"><?php esc_html_e( 'Edit', 'woo-gutenberg-products-block' ); ?></button> | <button type="button" class="button-link button-link-delete"><?php esc_html_e( 'Delete', 'woo-gutenberg-products-block' ); ?></button>
</div>
</td>
<td width="1%" class="wc-local-pickup-location-enabled">
<button type="button" class="enabled-toggle-button button-link">
<?php
echo ! empty( $location['enabled'] )
? '<span class="woocommerce-input-toggle woocommerce-input-toggle--enabled">' . esc_html__( 'Yes', 'woo-gutenberg-products-block' ) . '</span><input type="hidden" name="locationEnabled[]" value="1" />'
: '<span class="woocommerce-input-toggle woocommerce-input-toggle--disabled">' . esc_html__( 'No', 'woo-gutenberg-products-block' ) . '</span><input type="hidden" name="locationEnabled[]" value="0" />';
?>
</button>
</td>
<td class="wc-local-pickup-location-address editable">
<div class="view"><?php echo esc_html( implode( ', ', array_filter( $location['address'] ) ) ); ?></div>
<div class="edit" hidden>
<input type="text" name="address_1[]" value="<?php echo esc_attr( $location['address']['address_1'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'Address', 'woo-gutenberg-products-block' ); ?>" />
<input type="text" name="city[]" value="<?php echo esc_attr( $location['address']['city'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'City', 'woo-gutenberg-products-block' ); ?>" />
<select class="state-select" hidden></select>
<input type="text" class="state-input" name="state[]" value="<?php echo esc_attr( $location['address']['state'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'State', 'woo-gutenberg-products-block' ); ?>" />
<input type="text" name="postcode[]" value="<?php echo esc_attr( $location['address']['postcode'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'Postcode / ZIP', 'woo-gutenberg-products-block' ); ?>" />
<select class="country-select" name="country[]">
<option value="" disabled selected><?php esc_html_e( 'Country', 'woo-gutenberg-products-block' ); ?></option>
<?php foreach ( WC()->countries->get_countries() as $code => $label ) : ?>
<option <?php selected( $code, $location['address']['country'] ); ?> value="<?php echo esc_attr( $code ); ?>"><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
</div>
</td>
<td class="wc-local-pickup-location-details editable">
<div class="view"><?php echo wp_kses_post( wpautop( $location['details'] ?: '&mdash;' ) ); ?></div>
<div class="edit" hidden><textarea name="details[]" rows="3"><?php echo esc_attr( $location['details'] ?? '' ); ?></textarea></div>
</td>
<?php
return ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<h2>' . esc_html__( 'Local Pickup', 'woo-gutenberg-products-block' ) . '</h2>';
echo '<div class="wrap"><div id="wc-shipping-method-pickup-location-settings-container"></div></div>';
}
}

View File

@ -1,7 +1,8 @@
<?php
namespace Automattic\WooCommerce\Blocks\Shipping;
use Automattic\WooCommerce\StoreApi\Utilities\CartController;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
/**
* ShippingController class.
@ -9,16 +10,172 @@ use Automattic\WooCommerce\StoreApi\Utilities\CartController;
* @internal
*/
class ShippingController {
/**
* Instance of the asset API.
*
* @var AssetApi
*/
protected $asset_api;
/**
* Instance of the asset data registry.
*
* @var AssetDataRegistry
*/
protected $asset_data_registry;
/**
* Constructor.
*
* @param AssetApi $asset_api Instance of the asset API.
* @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
*/
public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
$this->asset_api = $asset_api;
$this->asset_data_registry = $asset_data_registry;
}
/**
* Initialization method.
*/
public function init() {
// @todo This should be moved inline for the settings page only.
$this->asset_data_registry->add(
'pickupLocationSettings',
get_option( 'woocommerce_pickup_location_settings', [] ),
true
);
$this->asset_data_registry->add(
'pickupLocations',
function() {
$locations = get_option( 'pickup_location_pickup_locations', [] );
$formatted = [];
foreach ( $locations as $location ) {
$formatted[] = [
'name' => $location['name'],
'address' => $location['address'],
'details' => $location['details'],
'enabled' => wc_string_to_bool( $location['enabled'] ),
];
}
return $formatted;
},
true
);
if ( is_admin() ) {
$this->asset_data_registry->add(
'countryStates',
function() {
return WC()->countries->get_states();
},
true
);
}
add_action( 'rest_api_init', [ $this, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
add_action( 'woocommerce_load_shipping_methods', array( $this, 'register_shipping_methods' ) );
add_filter( 'woocommerce_shipping_packages', array( $this, 'filter_shipping_packages' ) );
add_filter( 'woocommerce_local_pickup_methods', array( $this, 'filter_local_pickup_methods' ) );
add_filter( 'woocommerce_customer_taxable_address', array( $this, 'pickup_location_customer_tax_location' ) );
}
/**
* Register Local Pickup settings for rest api.
*/
public function register_settings() {
register_setting(
'options',
'woocommerce_pickup_location_settings',
[
'type' => 'object',
'description' => 'WooCommerce Local Pickup Method Settings',
'default' => [],
'show_in_rest' => [
'name' => 'pickup_location_settings',
'schema' => [
'type' => 'object',
'properties' => array(
'enabled' => [
'description' => __( 'If enabled, this method will appear on the block based checkout.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => [ 'yes', 'no' ],
],
'title' => [
'description' => __( 'This controls the title which the user sees during checkout.', 'woo-gutenberg-products-block' ),
'type' => 'string',
],
'tax_status' => [
'description' => __( 'If a cost is defined, this controls if taxes are applied to that cost.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => [ 'taxable', 'none' ],
],
'cost' => [
'description' => __( 'Optional cost to charge for local pickup.', 'woo-gutenberg-products-block' ),
'type' => 'string',
],
),
],
],
]
);
register_setting(
'options',
'pickup_location_pickup_locations',
[
'type' => 'array',
'description' => 'WooCommerce Local Pickup Locations',
'default' => [],
'show_in_rest' => [
'name' => 'pickup_locations',
'schema' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => array(
'name' => [
'type' => 'string',
],
'address' => [
'type' => 'object',
'properties' => array(
'address_1' => [
'type' => 'string',
],
'city' => [
'type' => 'string',
],
'state' => [
'type' => 'string',
],
'postcode' => [
'type' => 'string',
],
'country' => [
'type' => 'string',
],
),
],
'details' => [
'type' => 'string',
],
'enabled' => [
'type' => 'boolean',
],
),
],
],
],
]
);
}
/**
* Load admin scripts.
*/
public function admin_scripts() {
$this->asset_api->register_script( 'wc-shipping-method-pickup-location', 'build/wc-shipping-method-pickup-location.js', [], true );
}
/**
* Registers the local pickup method for blocks.
*/
@ -76,6 +233,7 @@ class ShippingController {
$chosen_method_id = explode( ':', $chosen_method )[0];
$chosen_method_instance = explode( ':', $chosen_method )[1] ?? 0;
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
if ( $chosen_method_id && true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && 'pickup_location' === $chosen_method_id ) {
$pickup_locations = get_option( 'pickup_location_pickup_locations', [] );

View File

@ -0,0 +1,266 @@
/**
* External dependencies
*/
import { switchUserToAdmin, visitAdminPage } from '@wordpress/e2e-test-utils';
import { findLabelWithText } from '@woocommerce/blocks-test-utils';
import { setOption } from '@wordpress/e2e-test-utils';
const goToSettingsPage = async () => {
await visitAdminPage(
'admin.php',
'page=wc-settings&tab=shipping&section=pickup_location'
);
await page.waitForSelector(
'#wc-shipping-method-pickup-location-settings-container'
);
};
const saveSettingsPageWithRefresh = async () => {
await expect( page ).toClick( 'button', {
text: 'Save changes',
} );
await expect( page ).toMatchElement( '.components-snackbar__content', {
text: 'Local Pickup settings have been saved.',
} );
await goToSettingsPage();
};
const setDefaults = async () => {
const enabledLabel = await findLabelWithText( 'Enable Local Pickup' );
const enabledChecked = await page.$eval(
'#inspector-checkbox-control-1',
( el ) => ( el as HTMLInputElement ).checked
);
if ( enabledChecked ) {
await enabledLabel.click();
}
await expect( page ).toFill(
'input[name="local_pickup_title"]',
'Local Pickup'
);
const costLabel = await findLabelWithText(
'Add a price for customers who choose local pickup'
);
const costChecked = await page.$eval(
'#inspector-checkbox-control-1',
( el ) => ( el as HTMLInputElement ).checked
);
if ( costChecked ) {
await costLabel.click();
}
};
const clearLocations = async () => {
const editButtons = await page.$$(
'.pickup-locations tbody tr .button-link-edit'
);
for ( const button of editButtons ) {
await button.click();
await expect( page ).toMatchElement( '.components-modal__content' );
await expect( page ).toClick( '.components-modal__content button', {
text: 'Delete location',
} );
await expect( page ).not.toMatchElement( '.components-modal__content' );
}
};
describe( `Local Pickup Settings`, () => {
beforeAll( async () => {
await switchUserToAdmin();
await goToSettingsPage();
await setDefaults();
await clearLocations();
await saveSettingsPageWithRefresh();
} );
afterAll( async () => {
await switchUserToAdmin();
await goToSettingsPage();
await setDefaults();
await clearLocations();
await saveSettingsPageWithRefresh();
} );
beforeEach( async () => {
await switchUserToAdmin();
await goToSettingsPage();
} );
it( 'renders without crashing', async () => {
await expect( page ).toMatchElement( '#local-pickup-settings' );
} );
describe( 'Global Settings', () => {
it( 'allows toggling of enabled on', async () => {
const initialChecked = await page.$eval(
'input[name="local_pickup_enabled"]',
( el ) => ( el as HTMLInputElement ).checked
);
const toggleLabel = await findLabelWithText(
'Enable Local Pickup'
);
await toggleLabel.click();
await saveSettingsPageWithRefresh();
expect(
await page.$eval(
'input[name="local_pickup_enabled"]',
( el ) => ( el as HTMLInputElement ).checked
)
).not.toBe( initialChecked );
} );
it( 'allows the title to be changed', async () => {
await expect( page ).toFill(
'input[name="local_pickup_title"]',
'Local Pickup Test'
);
await saveSettingsPageWithRefresh();
expect(
await page.$eval(
'input[name="local_pickup_title"]',
( el ) => el.value
)
).toBe( 'Local Pickup Test' );
} );
it( 'add price field toggles settings', async () => {
const selector = `input[name="local_pickup_cost"]`; // Cost field.
const toggleLabel = await findLabelWithText(
'Add a price for customers who choose local pickup'
);
await expect( toggleLabel ).toToggleElement( selector );
} );
it( 'cost and tax status can be set', async () => {
const toggleLabel = await findLabelWithText(
'Add a price for customers who choose local pickup'
);
const initialChecked = await page.$eval(
'#inspector-checkbox-control-1',
( el ) => ( el as HTMLInputElement ).checked
);
if ( ! initialChecked ) {
await toggleLabel.click();
}
await expect( page ).toFill(
'input[name="local_pickup_cost"]',
'20'
);
await page.select(
'select[name="local_pickup_tax_status"]',
'none'
);
await saveSettingsPageWithRefresh();
const refreshChecked = await page.$eval(
'#inspector-checkbox-control-1',
( el ) => ( el as HTMLInputElement ).checked
);
const costValue = await page.$eval(
'input[name="local_pickup_cost"]',
( el ) => el.value
);
const taxValue = await page.$eval(
'select[name="local_pickup_tax_status"]',
( el ) => el.value
);
expect( refreshChecked ).toBe( true );
expect( costValue ).toBe( '20' );
expect( taxValue ).toBe( 'none' );
} );
} );
describe( 'Location Settings', () => {
it( 'can add a new location', async () => {
const oldLocations = await page.$$eval(
'.pickup-locations tbody tr',
( rows ) => rows.length
);
await expect( page ).toClick( 'button', {
text: 'Add Pickup Location',
} );
await expect( page ).toMatchElement( '.components-modal__content' );
await expect( page ).toFill(
'.components-modal__content input[name="location_name"]',
'New Location Name'
);
await expect( page ).toFill(
'.components-modal__content input[name="location_address"]',
'New Address 123'
);
await expect( page ).toFill(
'.components-modal__content input[name="location_city"]',
'New City'
);
await page.select(
'.components-modal__content select[name="location_country"]',
'United Kingdom (UK)'
);
await expect( page ).toFill(
'.components-modal__content input[name="location_state"]',
'New State'
);
await expect( page ).toFill(
'.components-modal__content input[name="location_postcode"]',
'N3W 123'
);
await expect( page ).toFill(
'.components-modal__content input[name="pickup_details"]',
'These are the pickup details for the new location.'
);
await expect( page ).toClick( '.components-modal__content button', {
text: 'Done',
} );
await saveSettingsPageWithRefresh();
const locations = await page.$$eval(
'.pickup-locations tbody tr',
( rows ) => rows.length
);
expect( locations ).toBe( oldLocations + 1 );
} );
it( 'can edit a location', async () => {
await expect( page ).toClick( '.pickup-locations button', {
text: 'Edit',
} );
await expect( page ).toMatchElement( '.components-modal__content' );
await expect( page ).toClick( '.components-modal__content button', {
text: 'Done',
} );
} );
it( 'can delete a location', async () => {
const oldLocations = await page.$$eval(
'.pickup-locations tbody tr',
( rows ) => rows.length
);
await expect( page ).toClick( '.pickup-locations button', {
text: 'Edit',
} );
await expect( page ).toMatchElement( '.components-modal__content' );
await expect( page ).toClick( '.components-modal__content button', {
text: 'Delete location',
} );
await saveSettingsPageWithRefresh();
const locations = await page.$$eval(
'.pickup-locations tbody tr',
( rows ) => rows.length
);
expect( locations ).toBe( oldLocations - 1 );
} );
} );
} );