Display proper notices for segment actions

This updates some of the segment listing messages to include the name of
 the segment to avoid confusion, and allows for the possibility that
 bulk actions will work for some segments but not all, in which case we
 need to show both success and error messages.

MAILPOET-5395
This commit is contained in:
John Oleksowicz
2024-02-12 11:07:34 -06:00
committed by Aschepikov
parent 28ca417261
commit 6675a616b5
6 changed files with 127 additions and 72 deletions

View File

@@ -7,6 +7,13 @@ export type Response = {
meta?: any; // eslint-disable-line @typescript-eslint/no-explicit-any meta?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}; };
export interface DynamicSegmentResponse extends Response {
meta: {
count: number;
errors?: string[];
};
}
export type ErrorResponse = { export type ErrorResponse = {
errors: { errors: {
error?: string; error?: string;

View File

@@ -5,6 +5,7 @@ import { __, _n, sprintf } from '@wordpress/i18n';
import { DynamicSegment, DynamicSegmentAction } from '../types'; import { DynamicSegment, DynamicSegmentAction } from '../types';
import { MailPoet } from '../../../mailpoet'; import { MailPoet } from '../../../mailpoet';
import { storeName } from '../store'; import { storeName } from '../store';
import { DynamicSegmentResponse } from '../../../ajax';
async function bulkAction( async function bulkAction(
action: DynamicSegmentAction, action: DynamicSegmentAction,
@@ -13,51 +14,6 @@ async function bulkAction(
if (!action) { if (!action) {
return; return;
} }
let successMessage = '';
let errorMessage = '';
switch (action) {
case 'trash':
successMessage = sprintf(
/* translators: %d - number of segments */
_n(
'Segment moved to trash.',
'%d segments moved to trash.',
segments.length,
'mailpoet',
),
segments.length,
);
errorMessage = __('Error moving segment to trash.', 'mailpoet');
break;
case 'delete':
successMessage = sprintf(
/* translators: %d - number of segments */
_n(
'Segment permanently deleted.',
'%d segments permanently deleted.',
segments.length,
'mailpoet',
),
segments.length,
);
errorMessage = __('Error deleting segment.', 'mailpoet');
break;
case 'restore':
successMessage = sprintf(
/* translators: %d - number of segments */
_n(
'Segment restored.',
'%d segments restored.',
segments.length,
'mailpoet',
),
segments.length,
);
errorMessage = __('Error restoring segment.', 'mailpoet');
break;
default:
break;
}
void MailPoet.Ajax.post({ void MailPoet.Ajax.post({
api_version: 'v1', api_version: 'v1',
endpoint: 'dynamic_segments', endpoint: 'dynamic_segments',
@@ -69,14 +25,86 @@ async function bulkAction(
}, },
}, },
}) })
.then(() => { .then((response: DynamicSegmentResponse) => {
void dispatch(noticesStore).createSuccessNotice(successMessage); if (response.meta.errors && response.meta.errors.length > 0) {
void dispatch(storeName).loadDynamicSegments(); response.meta.errors.forEach(
(error: string) =>
void dispatch(noticesStore).createErrorNotice(error),
);
}
const count = response.meta.count;
if (count > 0) {
let successMessage = '';
switch (action) {
case 'trash':
successMessage = sprintf(
/* translators: %d - number of segments */
_n(
'Segment moved to trash.',
'%d segments moved to trash.',
count,
'mailpoet',
),
count,
);
break;
case 'delete':
successMessage = sprintf(
/* translators: %d - number of segments */
_n(
'Segment permanently deleted.',
'%d segments permanently deleted.',
count,
'mailpoet',
),
count,
);
break;
case 'restore':
successMessage = sprintf(
/* translators: %d - number of segments */
_n(
'Segment restored.',
'%d segments restored.',
count,
'mailpoet',
),
count,
);
break;
default:
break;
}
void dispatch(noticesStore).createSuccessNotice(successMessage);
void dispatch(storeName).loadDynamicSegments();
}
}) })
.fail(() => { .fail((response: ErrorResponse) => {
void dispatch(noticesStore).createErrorNotice(errorMessage, { let errorMessage = '';
explicitDismiss: true, if (response.errors) {
}); response.errors.forEach(
(error) =>
void dispatch(noticesStore).createErrorNotice(error.message),
{ explicitDismiss: true },
);
} else {
switch (action) {
case 'trash':
errorMessage = __('Error moving segment to trash.', 'mailpoet');
break;
case 'delete':
errorMessage = __('Error deleting segment.', 'mailpoet');
break;
case 'restore':
errorMessage = __('Error restoring segment.', 'mailpoet');
break;
default:
break;
}
void dispatch(noticesStore).createErrorNotice(errorMessage, {
explicitDismiss: true,
});
}
}); });
} }

View File

@@ -50,6 +50,7 @@ type Response = {
count: number; count: number;
segment: string; segment: string;
tag: string; tag: string;
errors?: string[];
}; };
}; };

View File

@@ -198,16 +198,9 @@ class DynamicSegments extends APIEndpoint {
]); ]);
} }
$activelyUsedNewslettersSubjects = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments([$segment->getId()]); $activelyUsedErrors = $this->getErrorMessagesForSegmentsUsedInActiveNewsletters([$segment->getId()]);
if (isset($activelyUsedNewslettersSubjects[$segment->getId()])) { if (count($activelyUsedErrors) > 0) {
return $this->badRequest([ return $this->badRequest($activelyUsedErrors);
Error::BAD_REQUEST => str_replace(
'%1$s',
"'" . join("', '", $activelyUsedNewslettersSubjects[$segment->getId()]) . "'",
// translators: %1$s is a comma-seperated list of emails for which the segment is used.
_x('Segment cannot be deleted because its used for %1$s email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet')
),
]);
} }
$this->segmentsRepository->bulkTrash([$segment->getId()], SegmentEntity::TYPE_DYNAMIC); $this->segmentsRepository->bulkTrash([$segment->getId()], SegmentEntity::TYPE_DYNAMIC);
@@ -217,6 +210,26 @@ class DynamicSegments extends APIEndpoint {
); );
} }
public function getErrorMessagesForSegmentsUsedInActiveNewsletters(array $segmentIds): array {
$errors = [];
$activelyUsedNewslettersSubjects = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($segmentIds);
foreach ($segmentIds as $segmentId) {
if (isset($activelyUsedNewslettersSubjects[$segmentId])) {
$segment = $this->getSegment(['id' => $segmentId]);
if ($segment) {
$errors[] = sprintf(
// translators: %1$s is the name of the segment, %2$s is a comma-seperated list of emails for which the segment is used.
_x('Segment \'%1$s\' cannot be deleted because its used for \'%2$s\' email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet'),
$segment->getName(),
join("', '", $activelyUsedNewslettersSubjects[$segmentId])
);
}
}
}
return $errors;
}
public function restore($data = []) { public function restore($data = []) {
if (!isset($data['id'])) { if (!isset($data['id'])) {
return $this->errorResponse([ return $this->errorResponse([
@@ -275,17 +288,22 @@ class DynamicSegments extends APIEndpoint {
public function bulkAction($data = []) { public function bulkAction($data = []) {
$definition = $this->listingHandler->getListingDefinition($data['listing']); $definition = $this->listingHandler->getListingDefinition($data['listing']);
$ids = $this->dynamicSegmentsListingRepository->getActionableIds($definition); $ids = $this->dynamicSegmentsListingRepository->getActionableIds($definition);
$meta = [];
if ($data['action'] === 'trash') { if ($data['action'] === 'trash') {
$count = $this->segmentsRepository->bulkTrash($ids, SegmentEntity::TYPE_DYNAMIC); $errors = $this->getErrorMessagesForSegmentsUsedInActiveNewsletters($ids);
if (count($errors) > 0) {
$meta['errors'] = $errors;
}
$meta['count'] = $this->segmentsRepository->bulkTrash($ids, SegmentEntity::TYPE_DYNAMIC);
} elseif ($data['action'] === 'restore') { } elseif ($data['action'] === 'restore') {
$count = $this->segmentsRepository->bulkRestore($ids, SegmentEntity::TYPE_DYNAMIC); $meta['count'] = $this->segmentsRepository->bulkRestore($ids, SegmentEntity::TYPE_DYNAMIC);
} elseif ($data['action'] === 'delete') { } elseif ($data['action'] === 'delete') {
$count = $this->segmentsRepository->bulkDelete($ids, SegmentEntity::TYPE_DYNAMIC); $meta['count'] = $this->segmentsRepository->bulkDelete($ids, SegmentEntity::TYPE_DYNAMIC);
} else { } else {
throw UnexpectedValueException::create() throw UnexpectedValueException::create()
->withErrors([Error::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]); ->withErrors([Error::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
} }
return $this->successResponse(null, ['count' => $count]); return $this->successResponse(null, $meta);
} }
private function getSegment(array $data): ?SegmentEntity { private function getSegment(array $data): ?SegmentEntity {

View File

@@ -308,12 +308,13 @@ class ManageSegmentsCest {
$i->amOnMailpoetPage('Segments'); $i->amOnMailpoetPage('Segments');
$i->waitForText($segmentTitle, 5, '[data-automation-id="mailpoet_dynamic_segment_name_' . $segment->getId() . '"]'); $i->waitForText($segmentTitle, 5, '[data-automation-id="mailpoet_dynamic_segment_name_' . $segment->getId() . '"]');
$i->clickWooTableActionInsideMoreButton($segmentTitle, 'Move to trash'); $i->clickWooTableActionInsideMoreButton($segmentTitle, 'Move to trash');
$i->waitForText("Segment cannot be deleted because its used for '{$subject}' email"); $i->click(['xpath' => '//button[text()="Trash"]']); // confirmation modal, xpath to avoid clicking the Trash tab
$i->waitForText("Segment '{$segmentTitle}' cannot be deleted because its used for '{$subject}' email");
$i->seeNoJSErrors(); $i->seeNoJSErrors();
$i->checkOption('[data-automation-id="listing-row-checkbox-' . $segment->getId() . '"]'); $i->checkWooTableCheckboxForItemName($segment->getName());
$i->waitForText('Move to trash'); $i->selectOption('Bulk Actions', 'Trash');
$i->click('Move to trash'); $i->click(['xpath' => '//button[text()="Trash"]']); // confirmation modal, xpath to avoid clicking the Trash tab
$i->waitForText('0 segments were moved to the trash.'); $i->waitForText("Segment '{$segmentTitle}' cannot be deleted because its used for '{$subject}' email");
} }
public function createUserSegmentAndCheckCount(\AcceptanceTester $i) { public function createUserSegmentAndCheckCount(\AcceptanceTester $i) {

View File

@@ -121,7 +121,7 @@ class DynamicSegmentsTest extends \MailPoetTest {
$response = $this->endpoint->trash(['id' => $dynamicSegment->getId()]); $response = $this->endpoint->trash(['id' => $dynamicSegment->getId()]);
$this->entityManager->refresh($dynamicSegment); $this->entityManager->refresh($dynamicSegment);
verify($response->status)->equals(APIResponse::STATUS_BAD_REQUEST); verify($response->status)->equals(APIResponse::STATUS_BAD_REQUEST);
verify($response->errors[0]['message'])->equals("Segment cannot be deleted because its used for 'Subject' email"); verify($response->errors[0]['message'])->equals("Segment '{$dynamicSegment->getName()}' cannot be deleted because its used for 'Subject' email");
} }
public function testItCanRestoreASegment() { public function testItCanRestoreASegment() {