no wp
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
export const namespace = 'woocommerce-google-analytics';
|
||||
export const actionPrefix = 'experimental__woocommerce_blocks';
|
||||
@@ -1,293 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { addAction } from '@wordpress/hooks';
|
||||
import type { ProductResponseItem, CartResponseItem } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { namespace, actionPrefix } from './constants';
|
||||
import {
|
||||
getProductFieldObject,
|
||||
getProductImpressionObject,
|
||||
trackEvent,
|
||||
trackCheckoutStep,
|
||||
trackCheckoutOption,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* Track customer progress through steps of the checkout. Triggers the event when the step changes:
|
||||
* 1 - Contact information
|
||||
* 2 - Shipping address
|
||||
* 3 - Billing address
|
||||
* 4 - Shipping options
|
||||
* 5 - Payment options
|
||||
*
|
||||
* @summary Track checkout progress with begin_checkout and checkout_progress
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#1_measure_checkout_steps
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-render-checkout-form`,
|
||||
namespace,
|
||||
trackCheckoutStep( 0 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-email-address`,
|
||||
namespace,
|
||||
trackCheckoutStep( 1 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-shipping-address`,
|
||||
namespace,
|
||||
trackCheckoutStep( 2 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-billing-address`,
|
||||
namespace,
|
||||
trackCheckoutStep( 3 )
|
||||
);
|
||||
|
||||
/**
|
||||
* Choose a shipping rate
|
||||
*
|
||||
* @summary Track the shipping rate being set using set_checkout_option
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-selected-shipping-rate`,
|
||||
namespace,
|
||||
( { shippingRateId }: { shippingRateId: string } ): void => {
|
||||
trackCheckoutOption( {
|
||||
step: 4,
|
||||
option: __( 'Shipping Method', 'woo-gutenberg-products-block' ),
|
||||
value: shippingRateId,
|
||||
} )();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Choose a payment method
|
||||
*
|
||||
* @summary Track the payment method being set using set_checkout_option
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-active-payment-method`,
|
||||
namespace,
|
||||
( { paymentMethodSlug }: { paymentMethodSlug: string } ): void => {
|
||||
trackCheckoutOption( {
|
||||
step: 5,
|
||||
option: __( 'Payment Method', 'woo-gutenberg-products-block' ),
|
||||
value: paymentMethodSlug,
|
||||
} )();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Add Payment Information
|
||||
*
|
||||
* This event signifies a user has submitted their payment information. Note, this is used to indicate checkout
|
||||
* submission, not `purchase` which is triggered on the thanks page.
|
||||
*
|
||||
* @summary Track the add_payment_info event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#add_payment_info
|
||||
*/
|
||||
addAction( `${ actionPrefix }-checkout-submit`, namespace, (): void => {
|
||||
trackEvent( 'add_payment_info' );
|
||||
} );
|
||||
|
||||
/**
|
||||
* Add to cart.
|
||||
*
|
||||
* This event signifies that an item was added to a cart for purchase.
|
||||
*
|
||||
* @summary Track the add_to_cart event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#add_to_cart
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-cart-add-item`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'add_to_cart', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __( 'Add to Cart', 'woo-gutenberg-products-block' ),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove item from the cart
|
||||
*
|
||||
* @summary Track the remove_from_cart event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#remove_from_cart
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-cart-remove-item`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: CartResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'remove_from_cart', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __(
|
||||
'Remove Cart Item',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Change cart item quantities
|
||||
*
|
||||
* @summary Custom change_cart_quantity event.
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-cart-set-item-quantity`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: CartResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'change_cart_quantity', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __(
|
||||
'Change Cart Item Quantity',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product List View
|
||||
*
|
||||
* @summary Track the view_item_list event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#view_item_list
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-list-render`,
|
||||
namespace,
|
||||
( {
|
||||
products,
|
||||
listName = __( 'Product List', 'woo-gutenberg-products-block' ),
|
||||
}: {
|
||||
products: Array< ProductResponseItem >;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
if ( products.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
trackEvent( 'view_item_list', {
|
||||
event_category: 'engagement',
|
||||
event_label: __(
|
||||
'Viewing products',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: products.map( ( product, index ) => ( {
|
||||
...getProductImpressionObject( product, listName ),
|
||||
list_position: index + 1,
|
||||
} ) ),
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product View Link Clicked
|
||||
*
|
||||
* @summary Track the select_content event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#select_content
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-view-link`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
listName,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
trackEvent( 'select_content', {
|
||||
content_type: 'product',
|
||||
items: [ getProductImpressionObject( product, listName ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product Search
|
||||
*
|
||||
* @summary Track the search event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#search
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-search`,
|
||||
namespace,
|
||||
( { searchTerm }: { searchTerm: string } ): void => {
|
||||
trackEvent( 'search', {
|
||||
search_term: searchTerm,
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Single Product View
|
||||
*
|
||||
* @summary Track the view_item event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#view_item
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-render`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
listName,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
if ( product ) {
|
||||
trackEvent( 'view_item', {
|
||||
items: [ getProductImpressionObject( product, listName ) ],
|
||||
} );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Track notices as Exception events.
|
||||
*
|
||||
* @summary Track the exception event
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/exceptions
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-store-notice-create`,
|
||||
namespace,
|
||||
( { status, content }: { status: string; content: string } ): void => {
|
||||
if ( status === 'error' ) {
|
||||
trackEvent( 'exception', {
|
||||
description: content,
|
||||
fatal: false,
|
||||
} );
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type {
|
||||
ProductResponseItem,
|
||||
CartResponseItem,
|
||||
StoreCart,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
interface ImpressionItem extends Gtag.Item {
|
||||
list_name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats data into the productFieldObject shape.
|
||||
*
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#product-data
|
||||
*/
|
||||
export const getProductFieldObject = (
|
||||
product: ProductResponseItem | CartResponseItem,
|
||||
quantity: number | undefined
|
||||
): Gtag.Item => {
|
||||
const productIdentifier = product.sku ? product.sku : '#' + product.id;
|
||||
const productCategory =
|
||||
'categories' in product && product.categories.length
|
||||
? product.categories[ 0 ].name
|
||||
: '';
|
||||
return {
|
||||
id: productIdentifier,
|
||||
name: product.name,
|
||||
quantity,
|
||||
category: productCategory,
|
||||
price: (
|
||||
parseInt( product.prices.price, 10 ) /
|
||||
10 ** product.prices.currency_minor_unit
|
||||
).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats data into the impressionFieldObject shape.
|
||||
*
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#impression-data
|
||||
*/
|
||||
export const getProductImpressionObject = (
|
||||
product: ProductResponseItem,
|
||||
listName: string
|
||||
): ImpressionItem => {
|
||||
const productIdentifier = product.sku ? product.sku : '#' + product.id;
|
||||
const productCategory = product.categories.length
|
||||
? product.categories[ 0 ].name
|
||||
: '';
|
||||
return {
|
||||
id: productIdentifier,
|
||||
name: product.name,
|
||||
list_name: listName,
|
||||
category: productCategory,
|
||||
price: (
|
||||
parseInt( product.prices.price, 10 ) /
|
||||
10 ** product.prices.currency_minor_unit
|
||||
).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Track an event using the global gtag function.
|
||||
*/
|
||||
export const trackEvent = (
|
||||
eventName: Gtag.EventNames | string,
|
||||
eventParams?: Gtag.ControlParams | Gtag.EventParams | Gtag.CustomParams
|
||||
): void => {
|
||||
if ( typeof gtag !== 'function' ) {
|
||||
throw new Error( 'Function gtag not implemented.' );
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log( `Tracking event ${ eventName }` );
|
||||
window.gtag( 'event', eventName, eventParams );
|
||||
};
|
||||
|
||||
let currentStep = -1;
|
||||
|
||||
export const trackCheckoutStep =
|
||||
( step: number ) =>
|
||||
( { storeCart }: { storeCart: StoreCart } ): void => {
|
||||
if ( currentStep === step ) {
|
||||
return;
|
||||
}
|
||||
trackEvent( step === 0 ? 'begin_checkout' : 'checkout_progress', {
|
||||
items: storeCart.cartItems.map( getProductFieldObject ),
|
||||
coupon: storeCart.cartCoupons[ 0 ]?.code || '',
|
||||
currency: storeCart.cartTotals.currency_code,
|
||||
value: (
|
||||
parseInt( storeCart.cartTotals.total_price, 10 ) /
|
||||
10 ** storeCart.cartTotals.currency_minor_unit
|
||||
).toString(),
|
||||
checkout_step: step,
|
||||
} );
|
||||
currentStep = step;
|
||||
};
|
||||
|
||||
export const trackCheckoutOption =
|
||||
( {
|
||||
step,
|
||||
option,
|
||||
value,
|
||||
}: {
|
||||
step: number;
|
||||
option: string;
|
||||
value: string;
|
||||
} ) =>
|
||||
(): void => {
|
||||
trackEvent( 'set_checkout_option', {
|
||||
checkout_step: step,
|
||||
checkout_option: option,
|
||||
value,
|
||||
} );
|
||||
currentStep = step;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export const PAYMENT_METHOD_NAME = 'bacs';
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getPaymentMethodData } from '@woocommerce/settings';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PAYMENT_METHOD_NAME } from './constants';
|
||||
|
||||
const settings = getPaymentMethodData( 'bacs', {} );
|
||||
const defaultLabel = __(
|
||||
'Direct bank transfer',
|
||||
'woocommerce'
|
||||
);
|
||||
const label = decodeEntities( settings?.title || '' ) || defaultLabel;
|
||||
|
||||
/**
|
||||
* Content component
|
||||
*/
|
||||
const Content = () => {
|
||||
return decodeEntities( settings.description || '' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Label component
|
||||
*
|
||||
* @param {*} props Props from payment API.
|
||||
*/
|
||||
const Label = ( props ) => {
|
||||
const { PaymentMethodLabel } = props.components;
|
||||
return <PaymentMethodLabel text={ label } />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bank transfer (BACS) payment method config object.
|
||||
*/
|
||||
const bankTransferPaymentMethod = {
|
||||
name: PAYMENT_METHOD_NAME,
|
||||
label: <Label />,
|
||||
content: <Content />,
|
||||
edit: <Content />,
|
||||
canMakePayment: () => true,
|
||||
ariaLabel: label,
|
||||
supports: {
|
||||
features: settings?.supports ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
registerPaymentMethod( bankTransferPaymentMethod );
|
||||
@@ -1 +0,0 @@
|
||||
export const PAYMENT_METHOD_NAME = 'cheque';
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getPaymentMethodData } from '@woocommerce/settings';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PAYMENT_METHOD_NAME } from './constants';
|
||||
|
||||
const settings = getPaymentMethodData( 'cheque', {} );
|
||||
const defaultLabel = __( 'Check payment', 'woocommerce' );
|
||||
const label = decodeEntities( settings?.title || '' ) || defaultLabel;
|
||||
|
||||
/**
|
||||
* Content component
|
||||
*/
|
||||
const Content = () => {
|
||||
return decodeEntities( settings.description || '' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Label component
|
||||
*
|
||||
* @param {*} props Props from payment API.
|
||||
*/
|
||||
const Label = ( props ) => {
|
||||
const { PaymentMethodLabel } = props.components;
|
||||
return <PaymentMethodLabel text={ label } />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cheque payment method config object.
|
||||
*/
|
||||
const offlineChequePaymentMethod = {
|
||||
name: PAYMENT_METHOD_NAME,
|
||||
label: <Label />,
|
||||
content: <Content />,
|
||||
edit: <Content />,
|
||||
canMakePayment: () => true,
|
||||
ariaLabel: label,
|
||||
supports: {
|
||||
features: settings?.supports ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
registerPaymentMethod( offlineChequePaymentMethod );
|
||||
@@ -1 +0,0 @@
|
||||
export const PAYMENT_METHOD_NAME = 'cod';
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getPaymentMethodData } from '@woocommerce/settings';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PAYMENT_METHOD_NAME } from './constants';
|
||||
|
||||
const settings = getPaymentMethodData( 'cod', {} );
|
||||
const defaultLabel = __( 'Cash on delivery', 'woocommerce' );
|
||||
const label = decodeEntities( settings?.title || '' ) || defaultLabel;
|
||||
|
||||
/**
|
||||
* Content component
|
||||
*/
|
||||
const Content = () => {
|
||||
return decodeEntities( settings.description || '' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Label component
|
||||
*
|
||||
* @param {*} props Props from payment API.
|
||||
*/
|
||||
const Label = ( props ) => {
|
||||
const { PaymentMethodLabel } = props.components;
|
||||
return <PaymentMethodLabel text={ label } />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine whether COD is available for this cart/order.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {boolean} props.cartNeedsShipping True if the cart contains any physical/shippable products.
|
||||
* @param {boolean} props.selectedShippingMethods
|
||||
*
|
||||
* @return {boolean} True if COD payment method should be displayed as a payment option.
|
||||
*/
|
||||
const canMakePayment = ( { cartNeedsShipping, selectedShippingMethods } ) => {
|
||||
if ( ! settings.enableForVirtual && ! cartNeedsShipping ) {
|
||||
// Store doesn't allow COD for virtual orders AND
|
||||
// order doesn't contain any shippable products.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! settings.enableForShippingMethods.length ) {
|
||||
// Store does not limit COD to specific shipping methods.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Look for a supported shipping method in the user's selected
|
||||
// shipping methods. If one is found, then COD is allowed.
|
||||
const selectedMethods = Object.values( selectedShippingMethods );
|
||||
// supported shipping methods might be global (eg. "Any flat rate"), hence
|
||||
// this is doing a `String.prototype.includes` match vs a `Array.prototype.includes` match.
|
||||
return settings.enableForShippingMethods.some( ( shippingMethodId ) => {
|
||||
return selectedMethods.some( ( selectedMethod ) => {
|
||||
return selectedMethod.includes( shippingMethodId );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Cash on Delivery (COD) payment method config object.
|
||||
*/
|
||||
const cashOnDeliveryPaymentMethod = {
|
||||
name: PAYMENT_METHOD_NAME,
|
||||
label: <Label />,
|
||||
content: <Content />,
|
||||
edit: <Content />,
|
||||
canMakePayment,
|
||||
ariaLabel: label,
|
||||
supports: {
|
||||
features: settings?.supports ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
registerPaymentMethod( cashOnDeliveryPaymentMethod );
|
||||
@@ -1 +0,0 @@
|
||||
export const PAYMENT_METHOD_NAME = 'paypal';
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getPaymentMethodData, WC_ASSET_URL } from '@woocommerce/settings';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PAYMENT_METHOD_NAME } from './constants';
|
||||
|
||||
const settings = getPaymentMethodData( 'paypal', {} );
|
||||
|
||||
/**
|
||||
* Content component
|
||||
*/
|
||||
const Content = () => {
|
||||
return decodeEntities( settings.description || '' );
|
||||
};
|
||||
|
||||
const paypalPaymentMethod = {
|
||||
name: PAYMENT_METHOD_NAME,
|
||||
label: (
|
||||
<img
|
||||
src={ `${ WC_ASSET_URL }/images/paypal.png` }
|
||||
alt={ decodeEntities(
|
||||
settings.title || __( 'PayPal', 'woocommerce' )
|
||||
) }
|
||||
/>
|
||||
),
|
||||
placeOrderButtonLabel: __(
|
||||
'Proceed to PayPal',
|
||||
'woocommerce'
|
||||
),
|
||||
content: <Content />,
|
||||
edit: <Content />,
|
||||
canMakePayment: () => true,
|
||||
ariaLabel: decodeEntities(
|
||||
settings?.title ||
|
||||
__( 'Payment via PayPal', 'woocommerce' )
|
||||
),
|
||||
supports: {
|
||||
features: settings.supports ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
registerPaymentMethod( paypalPaymentMethod );
|
||||
@@ -1,181 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SelectControl, TextControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { PickupLocation } from '../types';
|
||||
import { countryStateOptions, states } from '../utils';
|
||||
|
||||
const Form = ( {
|
||||
formRef,
|
||||
values,
|
||||
setValues,
|
||||
}: {
|
||||
formRef: React.RefObject< HTMLFormElement >;
|
||||
values: PickupLocation;
|
||||
setValues: React.Dispatch< React.SetStateAction< PickupLocation > >;
|
||||
} ) => {
|
||||
const { country: selectedCountry, state: selectedState } = values.address;
|
||||
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,
|
||||
},
|
||||
} ) );
|
||||
};
|
||||
|
||||
const countryHasStates =
|
||||
states[ selectedCountry ] &&
|
||||
Object.keys( states[ selectedCountry ] ).length > 0;
|
||||
|
||||
return (
|
||||
<form ref={ formRef }>
|
||||
<TextControl
|
||||
label={ __( 'Location name', 'woo-gutenberg-products-block' ) }
|
||||
name={ 'location_name' }
|
||||
value={ values.name }
|
||||
onChange={ setLocationField( 'name' ) }
|
||||
autoComplete="off"
|
||||
required={ true }
|
||||
onInvalid={ (
|
||||
event: React.InvalidEvent< HTMLInputElement >
|
||||
) => {
|
||||
event.target.setCustomValidity(
|
||||
__(
|
||||
'A Location title is required',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
);
|
||||
} }
|
||||
onInput={ ( event: React.ChangeEvent< HTMLInputElement > ) => {
|
||||
event.target.setCustomValidity( '' );
|
||||
} }
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
{ ! countryHasStates && (
|
||||
<TextControl
|
||||
placeholder={ __(
|
||||
'State',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
value={ selectedState }
|
||||
onChange={ setLocationAddressField( 'state' ) }
|
||||
/>
|
||||
) }
|
||||
<SelectControl
|
||||
name="location_country_state"
|
||||
label={ __(
|
||||
'Country / State',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
hideLabelFromVision={ true }
|
||||
placeholder={ __(
|
||||
'Country / State',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
value={ ( () => {
|
||||
if ( ! selectedState && countryHasStates ) {
|
||||
return `${ selectedCountry }:${
|
||||
Object.keys( states[ selectedCountry ] )[ 0 ]
|
||||
}`;
|
||||
}
|
||||
|
||||
return `${ selectedCountry }${
|
||||
selectedState &&
|
||||
states[ selectedCountry ]?.[ selectedState ]
|
||||
? ':' + selectedState
|
||||
: ''
|
||||
}`;
|
||||
} )() }
|
||||
onChange={ ( val: string ) => {
|
||||
const [ country, state = '' ] = val.split( ':' );
|
||||
setLocationAddressField( 'country' )( country );
|
||||
setLocationAddressField( 'state' )( state );
|
||||
} }
|
||||
>
|
||||
{ countryStateOptions.options.map( ( option ) => {
|
||||
if ( option.label ) {
|
||||
return (
|
||||
<optgroup
|
||||
key={ option.label }
|
||||
label={ option.label }
|
||||
>
|
||||
{ option.options.map( ( subOption ) => (
|
||||
<option
|
||||
key={ subOption.value }
|
||||
value={ subOption.value }
|
||||
>
|
||||
{ subOption.label }
|
||||
</option>
|
||||
) ) }
|
||||
</optgroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<option
|
||||
key={ option.options[ 0 ].value }
|
||||
value={ option.options[ 0 ].value }
|
||||
>
|
||||
{ option.options[ 0 ].label }
|
||||
</option>
|
||||
);
|
||||
} ) }
|
||||
</SelectControl>
|
||||
|
||||
<TextControl
|
||||
label={ __( 'Pickup details', 'woo-gutenberg-products-block' ) }
|
||||
name={ 'pickup_details' }
|
||||
value={ values.details }
|
||||
onChange={ setLocationField( 'details' ) }
|
||||
autoComplete="off"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* 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={ () => {
|
||||
const form =
|
||||
formRef?.current as unknown as HTMLFormElement;
|
||||
if ( form.reportValidity() ) {
|
||||
onSave( values );
|
||||
onClose();
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ __( 'Done', 'woo-gutenberg-products-block' ) }
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
formRef={ formRef }
|
||||
values={ values }
|
||||
setValues={ setValues }
|
||||
/>
|
||||
</SettingsModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditLocation;
|
||||
@@ -1,212 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createInterpolateElement, useState } from '@wordpress/element';
|
||||
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
|
||||
import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
|
||||
import {
|
||||
CheckboxControl,
|
||||
SelectControl,
|
||||
TextControl,
|
||||
ExternalLink,
|
||||
Notice,
|
||||
} from '@wordpress/components';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
/**
|
||||
* 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 StyledNotice = styled( Notice )`
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
`;
|
||||
|
||||
const GeneralSettings = () => {
|
||||
const { settings, setSettingField, readOnlySettings } =
|
||||
useSettingsContext();
|
||||
const [ showCosts, setShowCosts ] = useState( !! settings.cost );
|
||||
|
||||
const shippingCostRequiresAddress = getSetting< boolean >(
|
||||
'shippingCostRequiresAddress',
|
||||
false
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsSection Description={ GeneralSettingsDescription }>
|
||||
<SettingsCard>
|
||||
{ readOnlySettings.hasLegacyPickup && (
|
||||
<StyledNotice status="warning" isDismissible={ false }>
|
||||
{ createInterpolateElement(
|
||||
__(
|
||||
'Enabling this will produce duplicate options at checkout. Remove the local pickup shipping method from your <a>shipping zones</a>.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
{
|
||||
a: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
}
|
||||
) }
|
||||
</StyledNotice>
|
||||
) }
|
||||
<CheckboxControl
|
||||
checked={ settings.enabled }
|
||||
name="local_pickup_enabled"
|
||||
onChange={ setSettingField( 'enabled' ) }
|
||||
label={ __(
|
||||
'Enable local pickup',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
help={
|
||||
<span>
|
||||
{ __(
|
||||
'When enabled, local pickup will appear as an option on the block based checkout.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
{ shippingCostRequiresAddress ? (
|
||||
<>
|
||||
<br />
|
||||
{ __(
|
||||
'If local pickup is enabled, the "Hide shipping costs until an address is entered" setting will be ignored.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</>
|
||||
) : null }
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<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"
|
||||
required={ true }
|
||||
onInvalid={ (
|
||||
event: React.InvalidEvent< HTMLInputElement >
|
||||
) => {
|
||||
event.target.setCustomValidity(
|
||||
__(
|
||||
'Local pickup title is required',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
);
|
||||
} }
|
||||
onInput={ (
|
||||
event: React.ChangeEvent< HTMLInputElement >
|
||||
) => {
|
||||
event.target.setCustomValidity( '' );
|
||||
} }
|
||||
/>
|
||||
<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"
|
||||
pattern="[0-9]+\.?[0-9]*"
|
||||
min={ 0 }
|
||||
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;
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import type { UniqueIdentifier } from '@dnd-kit/core';
|
||||
import { 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';
|
||||
import { getUserFriendlyAddress } from './utils';
|
||||
|
||||
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/woocommerce-blocks-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,
|
||||
readOnlySettings,
|
||||
} = 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>
|
||||
{ getUserFriendlyAddress( row.address ) }
|
||||
</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[] );
|
||||
} }
|
||||
placeholder={ __(
|
||||
'When you add a pickup location, it will appear here.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
footerContent={ FooterContent }
|
||||
/>
|
||||
{ editingLocation && (
|
||||
<EditLocation
|
||||
locationData={
|
||||
editingLocation === 'new'
|
||||
? {
|
||||
name: '',
|
||||
details: '',
|
||||
enabled: true,
|
||||
address: {
|
||||
address_1: '',
|
||||
city: '',
|
||||
state: readOnlySettings.storeState,
|
||||
postcode: '',
|
||||
country: readOnlySettings.storeCountry,
|
||||
},
|
||||
}
|
||||
: 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;
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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={ (
|
||||
event: React.MouseEvent< HTMLButtonElement, MouseEvent >
|
||||
) => {
|
||||
event.preventDefault();
|
||||
const target = event.target as HTMLButtonElement;
|
||||
if ( target?.form?.reportValidity() ) {
|
||||
save();
|
||||
}
|
||||
} }
|
||||
type="submit"
|
||||
>
|
||||
{ __( 'Save changes', 'woo-gutenberg-products-block' ) }
|
||||
</Button>
|
||||
</SaveSectionWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveSettings;
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* 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 fastDeepEqual from 'fast-deep-equal/es6';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type {
|
||||
SortablePickupLocation,
|
||||
SettingsContextType,
|
||||
ShippingMethodSettings,
|
||||
} from './types';
|
||||
import {
|
||||
defaultSettings,
|
||||
getInitialSettings,
|
||||
defaultReadyOnlySettings,
|
||||
readOnlySettings,
|
||||
getInitialPickupLocations,
|
||||
} from './utils';
|
||||
|
||||
const SettingsContext = createContext< SettingsContextType >( {
|
||||
settings: defaultSettings,
|
||||
readOnlySettings: defaultReadyOnlySettings,
|
||||
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 (
|
||||
fastDeepEqual(
|
||||
response.pickup_location_settings,
|
||||
data.pickup_location_settings
|
||||
) &&
|
||||
fastDeepEqual(
|
||||
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,
|
||||
readOnlySettings,
|
||||
pickupLocations,
|
||||
setPickupLocations,
|
||||
toggleLocation,
|
||||
updateLocation,
|
||||
isSaving,
|
||||
save,
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={ settingsData }>
|
||||
{ children }
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 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.form`
|
||||
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;
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* 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 ReadOnlySettings = {
|
||||
storeCountry: string;
|
||||
storeState: string;
|
||||
hasLegacyPickup: boolean;
|
||||
};
|
||||
|
||||
export type SettingsContextType = {
|
||||
settings: ShippingMethodSettings;
|
||||
readOnlySettings: ReadOnlySettings;
|
||||
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;
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { cleanForSlug } from '@wordpress/url';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { isObject } from '@woocommerce/types';
|
||||
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: false,
|
||||
title: __( 'Local Pickup', 'woo-gutenberg-products-block' ),
|
||||
tax_status: 'taxable',
|
||||
cost: '',
|
||||
};
|
||||
|
||||
export const defaultReadyOnlySettings = {
|
||||
hasLegacyPickup: false,
|
||||
storeCountry: '',
|
||||
storeState: '',
|
||||
};
|
||||
declare global {
|
||||
const hydratedScreenSettings: {
|
||||
pickupLocationSettings: {
|
||||
enabled: string;
|
||||
title: string;
|
||||
tax_status: string;
|
||||
cost: string;
|
||||
};
|
||||
pickupLocations: PickupLocation[];
|
||||
readonlySettings: typeof defaultReadyOnlySettings;
|
||||
};
|
||||
}
|
||||
|
||||
export const getInitialSettings = (): ShippingMethodSettings => {
|
||||
const settings = hydratedScreenSettings.pickupLocationSettings;
|
||||
|
||||
return {
|
||||
enabled: settings?.enabled
|
||||
? settings?.enabled === 'yes'
|
||||
: defaultSettings.enabled,
|
||||
title: settings?.title || defaultSettings.title,
|
||||
tax_status: settings?.tax_status || defaultSettings.tax_status,
|
||||
cost: settings?.cost || defaultSettings.cost,
|
||||
};
|
||||
};
|
||||
|
||||
export const getInitialPickupLocations = (): SortablePickupLocation[] =>
|
||||
indexLocationsById( hydratedScreenSettings.pickupLocations || [] );
|
||||
|
||||
export const readOnlySettings =
|
||||
hydratedScreenSettings.readonlySettings || defaultReadyOnlySettings;
|
||||
|
||||
export const countries = getSetting< Record< string, string > >(
|
||||
'countries',
|
||||
[]
|
||||
);
|
||||
export const states = getSetting< Record< string, Record< string, string > > >(
|
||||
'countryStates',
|
||||
[]
|
||||
);
|
||||
export const getUserFriendlyAddress = ( address: unknown ) => {
|
||||
const updatedAddress = isObject( address ) && {
|
||||
...address,
|
||||
country:
|
||||
typeof address.country === 'string' && countries[ address.country ],
|
||||
state:
|
||||
typeof address.country === 'string' &&
|
||||
typeof address.state === 'string' &&
|
||||
states[ address.country ]?.[ address.state ]
|
||||
? states[ address.country ][ address.state ]
|
||||
: address.state,
|
||||
};
|
||||
|
||||
return Object.values( updatedAddress )
|
||||
.filter( ( value ) => value !== '' )
|
||||
.join( ', ' );
|
||||
};
|
||||
|
||||
// Outputs the list of countries and states in a single dropdown select.
|
||||
const countryStateDropdownOptions = () => {
|
||||
const countryStateOptions = Object.keys( countries ).map( ( country ) => {
|
||||
const countryStates = states[ country ] || {};
|
||||
|
||||
if ( Object.keys( countryStates ).length === 0 ) {
|
||||
return {
|
||||
options: [
|
||||
{
|
||||
value: country,
|
||||
label: countries[ country ],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const stateOptions = Object.keys( countryStates ).map( ( state ) => ( {
|
||||
value: `${ country }:${ state }`,
|
||||
label: `${ countries[ country ] } — ${ countryStates[ state ] }`,
|
||||
} ) );
|
||||
return {
|
||||
label: countries[ country ],
|
||||
options: [ ...stateOptions ],
|
||||
};
|
||||
} );
|
||||
return {
|
||||
options: countryStateOptions,
|
||||
};
|
||||
};
|
||||
export const countryStateOptions = countryStateDropdownOptions();
|
||||
@@ -1,4 +0,0 @@
|
||||
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';
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import styled from '@emotion/styled';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const StyledCard = styled( Card )`
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const StyledCardBody = styled( CardBody )`
|
||||
padding: 24px;
|
||||
|
||||
// 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: 1.5em;
|
||||
|
||||
// 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: ReactNode;
|
||||
} ): JSX.Element => (
|
||||
<StyledCard>
|
||||
<StyledCardBody { ...props }>{ children }</StyledCardBody>
|
||||
</StyledCard>
|
||||
);
|
||||
|
||||
export default SettingsCard;
|
||||
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Modal } from '@wordpress/components';
|
||||
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: none !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;
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from '@wordpress/element';
|
||||
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: none !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;
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* 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 style={ { width: '1%' } }>
|
||||
<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,
|
||||
placeholder,
|
||||
}: {
|
||||
columns: ColumnProps[];
|
||||
data: SortableData[];
|
||||
setData: ( data: SortableData[] ) => void;
|
||||
className?: string;
|
||||
placeholder?: string | ( () => JSX.Element );
|
||||
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>
|
||||
{ columns.map( ( column, index ) => (
|
||||
<th
|
||||
key={ column.name }
|
||||
{ ...getColumnProps(
|
||||
column,
|
||||
`sortable-table__column`
|
||||
) }
|
||||
colSpan={ index === 0 ? 2 : 1 }
|
||||
>
|
||||
{ column.label }
|
||||
</th>
|
||||
) ) }
|
||||
</tr>
|
||||
</thead>
|
||||
{ FooterContent && (
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={ columns.length + 1 }>
|
||||
<FooterContent />
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
) }
|
||||
<tbody>
|
||||
<SortableContext
|
||||
items={ items }
|
||||
strategy={ verticalListSortingStrategy }
|
||||
>
|
||||
{ !! data.length ? (
|
||||
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>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={ columns.length + 1 }>
|
||||
{ placeholder }
|
||||
</td>
|
||||
</tr>
|
||||
) }
|
||||
</SortableContext>
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableTable;
|
||||
Reference in New Issue
Block a user