Refactor used coupon filter to load coupons via API

[MAILPOET-5123]
This commit is contained in:
Jan Lysý
2023-09-18 12:59:52 +02:00
committed by Aschepikov
parent 191bd69fac
commit edb2c97286
2 changed files with 166 additions and 54 deletions

View File

@ -1,10 +1,12 @@
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { __ } from '@wordpress/i18n';
import { Grid } from 'common/grid'; import { Grid } from 'common/grid';
import { Select } from 'common'; import { Select } from 'common';
import { ReactSelect } from 'common/form/react_select/react_select'; import { ReactSelect } from 'common/form/react_select/react_select';
import { MailPoet } from 'mailpoet'; import { MailPoet } from 'mailpoet';
import { filter } from 'lodash/fp'; import { filter, uniqBy } from 'lodash/fp';
import { APIErrorsNotice } from '../../../../../notices/api_errors_notice';
import { import {
WooCommerceFormItem, WooCommerceFormItem,
FilterProps, FilterProps,
@ -36,6 +38,106 @@ export function UsedCouponCodeFields({
[filterIndex], [filterIndex],
); );
const { updateSegmentFilter } = useDispatch(storeName); const { updateSegmentFilter } = useDispatch(storeName);
// isInitialized is used for displaying loading message before loading coupons
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [coupons, setCoupons] = useState<SelectOption[]>([]);
// additionalLoading state is used for displaying loading message when loading more coupons
const [additionalLoading, setAdditionalLoading] = useState<boolean>(false);
const [page, setPage] = useState<number>(1);
// hasMore state is used to prevent loading more coupons when there are no more coupons to load
const [hasMore, setHasMore] = useState<boolean>(true);
const [searchQuery, setSearchQuery] = useState<string>('');
const [errors, setErrors] = useState([]);
const loadCoupons = useCallback(
(
isInitialLoading: boolean,
newPage: number,
newSearchQuery: string,
newHasMore: boolean,
) => {
if (!newHasMore) {
return;
}
if (!isInitialLoading) {
setAdditionalLoading(true);
}
void MailPoet.Ajax.post({
api_version: MailPoet.apiVersion,
endpoint: 'coupons',
action: 'getCoupons',
data: {
page_number: newPage,
page_size: 1000,
include_coupon_ids: segment.coupon_code_ids,
search: newSearchQuery,
},
})
.then((response) => {
const { data } = response;
const loadedCoupons: SelectOption[] = data.map((coupon: Coupon) => ({
value: coupon.id.toString(),
label: coupon.text,
}));
const nextPage = newPage + 1;
if (loadedCoupons.length === 0) {
setHasMore(false);
} else {
setCoupons((prevOptions: SelectOption[]) =>
uniqBy(
(coupon) => coupon.value,
[...prevOptions, ...loadedCoupons],
),
);
setPage(nextPage);
}
if (!isInitialLoading) {
setAdditionalLoading(false);
}
})
.fail((response: ErrorResponse) => {
setErrors(response.errors as { message: string }[]);
});
},
[segment.coupon_code_ids],
);
/**
* This function is called when user scrolls to the bottom of the select and should load more coupons.
* Loading coupons should not be called when there are no more coupons to load.
*/
const handleMenuScrollToBottom = () => {
if (!additionalLoading && hasMore) {
loadCoupons(false, page, searchQuery, hasMore);
}
};
/**
* Function for handling search input change that can filter coupons by search query when
* there is more coupons than one page.
* @param inputValue
*/
const handleInputChange = (inputValue: string): void => {
const oldSearchQuery = searchQuery; // OldSearchQuery is used to prevent loading coupons when search query is deleted
setSearchQuery(inputValue);
if (
!additionalLoading &&
((hasMore && inputValue) || (oldSearchQuery && !inputValue))
) {
setPage(1);
// Passing new values to loadCoupons to avoid using old values
loadCoupons(false, 1, inputValue, hasMore);
}
};
useEffect(() => {
if (!isInitialized) {
loadCoupons(true, page, searchQuery, hasMore);
setIsInitialized(true);
}
}, [isInitialized, page, searchQuery, loadCoupons, hasMore]);
useEffect(() => { useEffect(() => {
if (!Array.isArray(segment.coupon_code_ids)) { if (!Array.isArray(segment.coupon_code_ids)) {
@ -46,59 +148,69 @@ export function UsedCouponCodeFields({
} }
}, [updateSegmentFilter, segment, filterIndex]); }, [updateSegmentFilter, segment, filterIndex]);
const coupons: Coupon[] = useSelect(
(select) => select(storeName).getCoupons(),
[],
);
const couponOptions = coupons.map((coupon) => ({
value: coupon.id,
label: coupon.name,
}));
return ( return (
<> <>
<Grid.CenteredRow> {errors.length > 0 && <APIErrorsNotice errors={errors} />}
<Select {isInitialized ? (
isMaxContentWidth <>
key="select-operator-used-coupon-codes" <Grid.CenteredRow>
value={segment.operator} <Select
onChange={(e): void => { isMaxContentWidth
void updateSegmentFilter({ operator: e.target.value }, filterIndex); key="select-operator-used-coupon-codes"
}} value={segment.operator}
automationId="select-operator-used-coupon-code" onChange={(e): void => {
> void updateSegmentFilter(
<option value={AnyValueTypes.ANY}>{MailPoet.I18n.t('anyOf')}</option> { operator: e.target.value },
<option value={AnyValueTypes.ALL}>{MailPoet.I18n.t('allOf')}</option> filterIndex,
<option value={AnyValueTypes.NONE}> );
{MailPoet.I18n.t('noneOf')} }}
</option> automationId="select-operator-used-coupon-code"
</Select> >
<ReactSelect <option value={AnyValueTypes.ANY}>
key="select-coupon-codes" {MailPoet.I18n.t('anyOf')}
isFullWidth </option>
isMulti <option value={AnyValueTypes.ALL}>
placeholder={MailPoet.I18n.t('selectWooCouponCodes')} {MailPoet.I18n.t('allOf')}
options={couponOptions} </option>
value={filter((option) => { <option value={AnyValueTypes.NONE}>
if (!segment.coupon_code_ids) return undefined; {MailPoet.I18n.t('noneOf')}
return segment.coupon_code_ids.indexOf(option.value) !== -1; </option>
}, couponOptions)} </Select>
onChange={(options: SelectOption[]): void => { <ReactSelect
void updateSegmentFilter( key="select-coupon-codes"
{ isFullWidth
coupon_code_ids: (options || []).map( isMulti
(x: SelectOption) => x.value, placeholder={MailPoet.I18n.t('selectWooCouponCodes')}
), options={coupons}
}, value={filter((option) => {
filterIndex, if (!segment.coupon_code_ids) return undefined;
); return segment.coupon_code_ids.indexOf(option.value) !== -1;
}} }, coupons)}
automationId="select-shipping-methods" onInputChange={handleInputChange}
/> onChange={(options: SelectOption[]): void => {
</Grid.CenteredRow> void updateSegmentFilter(
<Grid.CenteredRow> {
<DaysPeriodField filterIndex={filterIndex} /> coupon_code_ids: (options || []).map(
</Grid.CenteredRow> (x: SelectOption) => x.value,
),
},
filterIndex,
);
}}
isLoading={additionalLoading}
automationId="select-shipping-methods"
onMenuScrollToBottom={handleMenuScrollToBottom}
/>
</Grid.CenteredRow>
<Grid.CenteredRow>
<DaysPeriodField filterIndex={filterIndex} />
</Grid.CenteredRow>
</>
) : (
<Grid.CenteredRow>
{__('Loading coupon codes...', 'mailpoet')}
</Grid.CenteredRow>
)}
</> </>
); );
} }

View File

@ -372,7 +372,7 @@ export type WooShippingMethod = {
export type Coupon = { export type Coupon = {
id: string; id: string;
name: string; text: string;
}; };
export enum SegmentTemplateCategories { export enum SegmentTemplateCategories {