From edb2c97286fbdbbcb2bc773eb86522f68590af0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Lys=C3=BD?= Date: Mon, 18 Sep 2023 12:59:52 +0200 Subject: [PATCH] Refactor used coupon filter to load coupons via API [MAILPOET-5123] --- .../fields/woocommerce/used_coupon_code.tsx | 218 +++++++++++++----- .../assets/js/src/segments/dynamic/types.ts | 2 +- 2 files changed, 166 insertions(+), 54 deletions(-) diff --git a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/fields/woocommerce/used_coupon_code.tsx b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/fields/woocommerce/used_coupon_code.tsx index 1684bfb1a0..167fdac5f0 100644 --- a/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/fields/woocommerce/used_coupon_code.tsx +++ b/mailpoet/assets/js/src/segments/dynamic/dynamic_segments_filters/fields/woocommerce/used_coupon_code.tsx @@ -1,10 +1,12 @@ 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 { Select } from 'common'; import { ReactSelect } from 'common/form/react_select/react_select'; import { MailPoet } from 'mailpoet'; -import { filter } from 'lodash/fp'; +import { filter, uniqBy } from 'lodash/fp'; +import { APIErrorsNotice } from '../../../../../notices/api_errors_notice'; import { WooCommerceFormItem, FilterProps, @@ -36,6 +38,106 @@ export function UsedCouponCodeFields({ [filterIndex], ); const { updateSegmentFilter } = useDispatch(storeName); + // isInitialized is used for displaying loading message before loading coupons + const [isInitialized, setIsInitialized] = useState(false); + const [coupons, setCoupons] = useState([]); + // additionalLoading state is used for displaying loading message when loading more coupons + const [additionalLoading, setAdditionalLoading] = useState(false); + const [page, setPage] = useState(1); + // hasMore state is used to prevent loading more coupons when there are no more coupons to load + const [hasMore, setHasMore] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + 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(() => { if (!Array.isArray(segment.coupon_code_ids)) { @@ -46,59 +148,69 @@ export function UsedCouponCodeFields({ } }, [updateSegmentFilter, segment, filterIndex]); - const coupons: Coupon[] = useSelect( - (select) => select(storeName).getCoupons(), - [], - ); - const couponOptions = coupons.map((coupon) => ({ - value: coupon.id, - label: coupon.name, - })); - return ( <> - - - { - if (!segment.coupon_code_ids) return undefined; - return segment.coupon_code_ids.indexOf(option.value) !== -1; - }, couponOptions)} - onChange={(options: SelectOption[]): void => { - void updateSegmentFilter( - { - coupon_code_ids: (options || []).map( - (x: SelectOption) => x.value, - ), - }, - filterIndex, - ); - }} - automationId="select-shipping-methods" - /> - - - - + {errors.length > 0 && } + {isInitialized ? ( + <> + + + { + if (!segment.coupon_code_ids) return undefined; + return segment.coupon_code_ids.indexOf(option.value) !== -1; + }, coupons)} + onInputChange={handleInputChange} + onChange={(options: SelectOption[]): void => { + void updateSegmentFilter( + { + coupon_code_ids: (options || []).map( + (x: SelectOption) => x.value, + ), + }, + filterIndex, + ); + }} + isLoading={additionalLoading} + automationId="select-shipping-methods" + onMenuScrollToBottom={handleMenuScrollToBottom} + /> + + + + + + ) : ( + + {__('Loading coupon codes...', 'mailpoet')} + + )} ); } diff --git a/mailpoet/assets/js/src/segments/dynamic/types.ts b/mailpoet/assets/js/src/segments/dynamic/types.ts index 80387a7d75..b7852696b8 100644 --- a/mailpoet/assets/js/src/segments/dynamic/types.ts +++ b/mailpoet/assets/js/src/segments/dynamic/types.ts @@ -372,7 +372,7 @@ export type WooShippingMethod = { export type Coupon = { id: string; - name: string; + text: string; }; export enum SegmentTemplateCategories {