Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add purchase date dynamic filter [MAILPOET-4986] #4760

Merged
merged 15 commits into from
Apr 3, 2023
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { useEffect } from 'react';
import { isValid, parseISO } from 'date-fns';
import { useSelect, useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';

import { MailPoet } from 'mailpoet';
import { Select } from 'common/form/select/select';
import { Datepicker } from 'common/datepicker/datepicker';
import { Grid } from 'common/grid';
import { Input } from 'common/form/input/input';

import { WordpressRoleFormItem } from '../types';
import { DateFormItem } from '../types';
import { storeName } from '../store';

export enum SubscribedDateOperator {
export enum DateOperator {
BEFORE = 'before',
AFTER = 'after',
ON = 'on',
Expand All @@ -21,12 +21,12 @@ export enum SubscribedDateOperator {
}

const availableOperators = [
SubscribedDateOperator.BEFORE,
SubscribedDateOperator.AFTER,
SubscribedDateOperator.ON,
SubscribedDateOperator.NOT_ON,
SubscribedDateOperator.IN_THE_LAST,
SubscribedDateOperator.NOT_IN_THE_LAST,
DateOperator.BEFORE,
DateOperator.AFTER,
DateOperator.ON,
DateOperator.NOT_ON,
DateOperator.IN_THE_LAST,
DateOperator.NOT_IN_THE_LAST,
];

const convertDateToString = (
Expand All @@ -53,8 +53,8 @@ type Props = {
filterIndex: number;
};

export function SubscribedDateFields({ filterIndex }: Props): JSX.Element {
const segment: WordpressRoleFormItem = useSelect(
export function DateFields({ filterIndex }: Props): JSX.Element {
const segment: DateFormItem = useSelect(
(select) => select(storeName).getSegmentFilter(filterIndex),
[filterIndex],
);
Expand All @@ -63,19 +63,14 @@ export function SubscribedDateFields({ filterIndex }: Props): JSX.Element {
useDispatch(storeName);

useEffect(() => {
if (
!availableOperators.includes(segment.operator as SubscribedDateOperator)
) {
void updateSegmentFilter(
{ operator: SubscribedDateOperator.BEFORE },
filterIndex,
);
if (!availableOperators.includes(segment.operator as DateOperator)) {
void updateSegmentFilter({ operator: DateOperator.BEFORE }, filterIndex);
}
if (
(segment.operator === SubscribedDateOperator.BEFORE ||
segment.operator === SubscribedDateOperator.AFTER ||
segment.operator === SubscribedDateOperator.ON ||
segment.operator === SubscribedDateOperator.NOT_ON) &&
(segment.operator === DateOperator.BEFORE ||
segment.operator === DateOperator.AFTER ||
segment.operator === DateOperator.ON ||
segment.operator === DateOperator.NOT_ON) &&
(parseDate(segment.value) === undefined ||
!/^\d+-\d+-\d+$/.test(segment.value))
) {
Expand All @@ -85,8 +80,8 @@ export function SubscribedDateFields({ filterIndex }: Props): JSX.Element {
);
}
if (
(segment.operator === SubscribedDateOperator.IN_THE_LAST ||
segment.operator === SubscribedDateOperator.NOT_IN_THE_LAST) &&
(segment.operator === DateOperator.IN_THE_LAST ||
segment.operator === DateOperator.NOT_IN_THE_LAST) &&
typeof segment.value === 'string' &&
!/^\d*$/.exec(segment.value)
) {
Expand All @@ -103,29 +98,21 @@ export function SubscribedDateFields({ filterIndex }: Props): JSX.Element {
void updateSegmentFilterFromEvent('operator', filterIndex, e);
}}
>
<option value={SubscribedDateOperator.BEFORE}>
{MailPoet.I18n.t('before')}
</option>
<option value={SubscribedDateOperator.AFTER}>
{MailPoet.I18n.t('after')}
</option>
<option value={SubscribedDateOperator.ON}>
{MailPoet.I18n.t('on')}
</option>
<option value={SubscribedDateOperator.NOT_ON}>
{MailPoet.I18n.t('notOn')}
</option>
<option value={SubscribedDateOperator.IN_THE_LAST}>
<option value={DateOperator.BEFORE}>{MailPoet.I18n.t('before')}</option>
<option value={DateOperator.AFTER}>{MailPoet.I18n.t('after')}</option>
<option value={DateOperator.ON}>{MailPoet.I18n.t('on')}</option>
<option value={DateOperator.NOT_ON}>{MailPoet.I18n.t('notOn')}</option>
<option value={DateOperator.IN_THE_LAST}>
{MailPoet.I18n.t('inTheLast')}
</option>
<option value={SubscribedDateOperator.NOT_IN_THE_LAST}>
<option value={DateOperator.NOT_IN_THE_LAST}>
{MailPoet.I18n.t('notInTheLast')}
</option>
</Select>
{(segment.operator === SubscribedDateOperator.BEFORE ||
segment.operator === SubscribedDateOperator.AFTER ||
segment.operator === SubscribedDateOperator.ON ||
segment.operator === SubscribedDateOperator.NOT_ON) && (
{(segment.operator === DateOperator.BEFORE ||
segment.operator === DateOperator.AFTER ||
segment.operator === DateOperator.ON ||
segment.operator === DateOperator.NOT_ON) && (
<Datepicker
dateFormat="MMM d, yyyy"
onChange={(value): void => {
Expand All @@ -137,8 +124,8 @@ export function SubscribedDateFields({ filterIndex }: Props): JSX.Element {
selected={segment.value ? parseDate(segment.value) : undefined}
/>
)}
{(segment.operator === SubscribedDateOperator.IN_THE_LAST ||
segment.operator === SubscribedDateOperator.NOT_IN_THE_LAST) && (
{(segment.operator === DateOperator.IN_THE_LAST ||
segment.operator === DateOperator.NOT_IN_THE_LAST) && (
<>
<Input
key="input"
Expand All @@ -156,3 +143,32 @@ export function SubscribedDateFields({ filterIndex }: Props): JSX.Element {
</Grid.CenteredRow>
);
}

export function dateFieldValidator(formItems: DateFormItem): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, a super-tiny observation is that I'd include a verb in the method name, e.g. validateDateField or isDateFieldValid. The current name sounds a bit like a service. Anyway, no need to fix it now.

Btw. do we need to validate that the date makes sense for any reason? Like that month is not 13, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing this out, @JanJakes! I would like to move this PR forward so I'm going to hold off on more changes for now, but I will make a note to refactor this when I work on one of the other segmentation tickets.

As for the 13, I think it's very unlikely we would get such a value because we're relying on the date picker, and I'm also just re-using existing logic that we haven't been having issues with so far to my knowledge, so I'm inclined to leave it as is.

if (!formItems.operator || !formItems.value) {
return false;
}

if (
[
DateOperator.BEFORE,
DateOperator.AFTER,
DateOperator.ON,
DateOperator.NOT_ON,
].includes(formItems.operator as DateOperator)
) {
const re = /^\d+-\d+-\d+$/;
return re.test(formItems.value);
}

if (
[DateOperator.IN_THE_LAST, DateOperator.NOT_IN_THE_LAST].includes(
formItems.operator as DateOperator,
)
) {
const re = /^\d+$/;
return re.test(formItems.value) && Number(formItems.value) > 0;
}

return false;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { useSelect } from '@wordpress/data';

import { WordpressRoleFormItem, SubscriberActionTypes } from '../types';
import { SubscriberActionTypes, WordpressRoleFormItem } from '../types';
import { storeName } from '../store';
import { WordpressRoleFields } from './subscriber_wordpress_role';
import {
SubscriberScoreFields,
validateSubscriberScore,
} from './subscriber_score';
import {
SubscribedDateFields,
SubscribedDateOperator,
} from './subscriber_subscribed_date';
import { DateFields, dateFieldValidator, DateOperator } from './date_fields';
import {
MailPoetCustomFields,
validateMailPoetCustomField,
Expand Down Expand Up @@ -47,28 +44,17 @@ export function validateSubscriber(formItems: WordpressRoleFormItem): boolean {
return false;
}
if (
formItems.operator === SubscribedDateOperator.BEFORE ||
formItems.operator === SubscribedDateOperator.AFTER ||
formItems.operator === SubscribedDateOperator.ON ||
formItems.operator === SubscribedDateOperator.NOT_ON
) {
const re = /^\d+-\d+-\d+$/;
return re.test(formItems.value);
}
if (
formItems.operator === SubscribedDateOperator.IN_THE_LAST ||
formItems.operator === SubscribedDateOperator.NOT_IN_THE_LAST
Object.values(DateOperator).includes(formItems.operator as DateOperator)
) {
const re = /^\d+$/;
return re.test(formItems.value) && Number(formItems.value) > 0;
return dateFieldValidator(formItems);
}
return false;
}

const componentsMap = {
[SubscriberActionTypes.WORDPRESS_ROLE]: WordpressRoleFields,
[SubscriberActionTypes.SUBSCRIBER_SCORE]: SubscriberScoreFields,
[SubscriberActionTypes.SUBSCRIBED_DATE]: SubscribedDateFields,
[SubscriberActionTypes.SUBSCRIBED_DATE]: DateFields,
[SubscriberActionTypes.MAILPOET_CUSTOM_FIELD]: MailPoetCustomFields,
[SubscriberActionTypes.SUBSCRIBED_TO_LIST]: SubscribedToList,
[SubscriberActionTypes.SUBSCRIBER_TAG]: SubscriberTag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MailPoet } from 'mailpoet';
import { filter } from 'lodash/fp';
import { ReactSelect } from 'common/form/react_select/react_select';
import { Select } from 'common/form/select/select';
import { useSelect, useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';

import { Grid } from 'common/grid';
import { Input } from 'common/form/input/input';
Expand All @@ -15,10 +15,11 @@ import {
WindowWooCommerceCountries,
WooCommerceFormItem,
} from '../types';
import { DateFields, dateFieldValidator, DateOperator } from './date_fields';
import { storeName } from '../store';
import {
WooCommerceActionTypes,
actionTypesWithDefaultTypeAny,
WooCommerceActionTypes,
} from './woocommerce_options';

export function validateWooCommerce(formItems: WooCommerceFormItem): boolean {
Expand Down Expand Up @@ -73,6 +74,11 @@ export function validateWooCommerce(formItems: WooCommerceFormItem): boolean {
) {
return false;
}
if (
Object.values(DateOperator).includes(formItems.operator as DateOperator)
) {
return dateFieldValidator(formItems);
}
if (
formItems.action === WooCommerceActionTypes.SINGLE_ORDER_VALUE &&
(!formItems.single_order_value_amount ||
Expand Down Expand Up @@ -284,6 +290,8 @@ export const WooCommerceFields: FunctionComponent<Props> = ({
</Grid.CenteredRow>
</>
);
} else if (segment.action === WooCommerceActionTypes.PURCHASE_DATE) {
optionFields = DateFields({ filterIndex });
} else if (segment.action === WooCommerceActionTypes.NUMBER_OF_ORDERS) {
optionFields = (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SegmentTypes } from '../types';
export enum WooCommerceActionTypes {
NUMBER_OF_ORDERS = 'numberOfOrders',
PURCHASED_CATEGORY = 'purchasedCategory',
PURCHASE_DATE = 'purchaseDate',
PURCHASED_PRODUCT = 'purchasedProduct',
TOTAL_SPENT = 'totalSpent',
CUSTOMER_IN_COUNTRY = 'customerInCountry',
Expand All @@ -27,6 +28,11 @@ export const WooCommerceOptions = [
label: MailPoet.I18n.t('wooPurchasedCategory'),
group: SegmentTypes.WooCommerce,
},
{
value: WooCommerceActionTypes.PURCHASE_DATE,
label: MailPoet.I18n.t('wooPurchaseDate'),
group: SegmentTypes.WooCommerce,
},
{
value: WooCommerceActionTypes.PURCHASED_PRODUCT,
label: MailPoet.I18n.t('wooPurchasedProduct'),
Expand Down
6 changes: 6 additions & 0 deletions mailpoet/assets/js/src/segments/dynamic/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export interface FormItem {
action?: string;
}

export interface DateFormItem extends FormItem {
operator?: string;
value?: string;
}

export interface WordpressRoleFormItem extends FormItem {
wordpressRole?: string[];
operator?: string;
Expand Down Expand Up @@ -116,6 +121,7 @@ export type Segment = {
};

export type AnyFormItem =
| DateFormItem
| WordpressRoleFormItem
| WooCommerceFormItem
| WooCommerceSubscriptionFormItem
Expand Down
4 changes: 4 additions & 0 deletions mailpoet/lib/DI/ContainerConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,11 @@ public function configure(ContainerBuilder $container) {
$container->autowire(\MailPoet\Segments\DynamicSegments\FilterFactory::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\FilterHandler::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailAction::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\FilterHelper::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\SubscriberScore::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedDate::class)->setPublic(true);
Expand All @@ -398,9 +400,11 @@ public function configure(ContainerBuilder $container) {
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooFilterHelper::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\SegmentSaveController::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\FilterDataMapper::class)->setPublic(true);
// Services
Expand Down
7 changes: 6 additions & 1 deletion mailpoet/lib/Segments/DynamicSegments/FilterDataMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
use MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper;
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny;
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
Expand All @@ -17,6 +18,7 @@
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent;
Expand Down Expand Up @@ -91,7 +93,7 @@ private function createSubscriber(array $data): DynamicSegmentFilterData {
if (empty($data['value'])) throw new InvalidFilterException('Missing number of days', InvalidFilterException::MISSING_VALUE);
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
'value' => $data['value'],
'operator' => $data['operator'] ?? SubscriberSubscribedDate::BEFORE,
'operator' => $data['operator'] ?? DateFilterHelper::BEFORE,
'connect' => $data['connect'],
]);
}
Expand Down Expand Up @@ -262,6 +264,9 @@ private function createWooCommerce(array $data): DynamicSegmentFilterData {
$filterData['single_order_value_type'] = $data['single_order_value_type'];
$filterData['single_order_value_amount'] = $data['single_order_value_amount'];
$filterData['single_order_value_days'] = $data['single_order_value_days'];
} elseif ($data['action'] === WooCommercePurchaseDate::ACTION) {
$filterData['operator'] = $data['operator'];
$filterData['value'] = $data['value'];
} else {
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
}
Expand Down