Add ability to duplicate dynamic segments

MAILPOET-4635
This commit is contained in:
John Oleksowicz
2023-05-11 16:10:25 -05:00
committed by Aschepikov
parent c0ffcbac9b
commit e636537580
7 changed files with 121 additions and 1 deletions

View File

@ -4,6 +4,7 @@ import ReactStringReplace from 'react-string-replace';
import { MailPoet } from 'mailpoet'; import { MailPoet } from 'mailpoet';
import { Listing } from 'listing/listing.jsx'; import { Listing } from 'listing/listing.jsx';
import { escapeHTML } from '@wordpress/escape-html';
const columns = [ const columns = [
{ {
@ -89,6 +90,36 @@ const itemActions = [
), ),
display: (item) => !item.is_plugin_missing, display: (item) => !item.is_plugin_missing,
}, },
{
name: 'duplicate_segment',
className: 'mailpoet-hide-on-mobile',
label: MailPoet.I18n.t('duplicate'),
onClick: (item, refresh) =>
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'dynamic_segments',
action: 'duplicate',
data: {
id: item.id,
},
})
.done((response) => {
MailPoet.Notice.success(
MailPoet.I18n.t('segmentDuplicated').replace(
'%1$s',
escapeHTML(response.data.name),
{ scroll: true },
),
);
refresh();
})
.fail((response) => {
MailPoet.Notice.error(
response.errors.map((error) => error.message),
{ scroll: true },
);
}),
},
{ {
name: 'edit_disabled', name: 'edit_disabled',
className: 'mailpoet-hide-on-mobile mailpoet-disabled', className: 'mailpoet-hide-on-mobile mailpoet-disabled',

View File

@ -19,6 +19,7 @@ use MailPoet\Segments\DynamicSegments\SegmentSaveController;
use MailPoet\Segments\SegmentsRepository; use MailPoet\Segments\SegmentsRepository;
use MailPoet\Segments\SegmentSubscribersRepository; use MailPoet\Segments\SegmentSubscribersRepository;
use MailPoet\UnexpectedValueException; use MailPoet\UnexpectedValueException;
use Throwable;
class DynamicSegments extends APIEndpoint { class DynamicSegments extends APIEndpoint {
@ -124,6 +125,29 @@ class DynamicSegments extends APIEndpoint {
} }
} }
public function duplicate($data = []) {
$segment = $this->getSegment($data);
if ($segment instanceof SegmentEntity) {
try {
$duplicate = $this->saveController->duplicate($segment);
} catch (Throwable $e) {
return $this->errorResponse([
// translators: %s is the error message
Error::UNKNOWN => sprintf(__('Duplicating of segment failed: %s', 'mailpoet'), $e->getMessage()),
], [], Response::STATUS_UNKNOWN);
}
return $this->successResponse(
$this->segmentsResponseBuilder->build($duplicate),
['count' => 1]
);
} else {
return $this->errorResponse([
Error::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
}
private function getErrorString(InvalidFilterException $e) { private function getErrorString(InvalidFilterException $e) {
switch ($e->getCode()) { switch ($e->getCode()) {
case InvalidFilterException::MISSING_TYPE: case InvalidFilterException::MISSING_TYPE:

View File

@ -38,6 +38,11 @@ class DynamicSegmentFilterEntity {
$this->filterData = $filterData; $this->filterData = $filterData;
} }
public function __clone() {
$this->id = null;
$this->segment = null;
}
/** /**
* @return SegmentEntity|null * @return SegmentEntity|null
*/ */

View File

@ -87,6 +87,7 @@ class SegmentEntity {
public function __clone() { public function __clone() {
// reset ID // reset ID
$this->id = null; $this->id = null;
$this->dynamicFilters = new ArrayCollection();
} }
/** /**

View File

@ -6,6 +6,7 @@ use MailPoet\ConflictException;
use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SegmentEntity;
use MailPoet\NotFoundException; use MailPoet\NotFoundException;
use MailPoet\Segments\SegmentsRepository; use MailPoet\Segments\SegmentsRepository;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use MailPoetVendor\Doctrine\ORM\ORMException; use MailPoetVendor\Doctrine\ORM\ORMException;
class SegmentSaveController { class SegmentSaveController {
@ -15,12 +16,17 @@ class SegmentSaveController {
/** @var FilterDataMapper */ /** @var FilterDataMapper */
private $filterDataMapper; private $filterDataMapper;
/** @var EntityManager */
private $entityManager;
public function __construct( public function __construct(
SegmentsRepository $segmentsRepository, SegmentsRepository $segmentsRepository,
FilterDataMapper $filterDataMapper FilterDataMapper $filterDataMapper,
EntityManager $entityManager
) { ) {
$this->segmentsRepository = $segmentsRepository; $this->segmentsRepository = $segmentsRepository;
$this->filterDataMapper = $filterDataMapper; $this->filterDataMapper = $filterDataMapper;
$this->entityManager = $entityManager;
} }
/** /**
@ -37,4 +43,23 @@ class SegmentSaveController {
return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DYNAMIC, $filtersData, $id); return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DYNAMIC, $filtersData, $id);
} }
public function duplicate(SegmentEntity $segmentEntity): SegmentEntity {
$duplicate = clone $segmentEntity;
// translators: %s is the name of the segment
$duplicate->setName(sprintf(__('Copy of %s', 'mailpoet'), $segmentEntity->getName()));
$this->segmentsRepository->verifyNameIsUnique($duplicate->getName(), $duplicate->getId());
$this->entityManager->wrapInTransaction(function(EntityManager $entityManager) use ($duplicate, $segmentEntity) {
foreach ($segmentEntity->getDynamicFilters() as $dynamicFilter) {
$duplicateFilter = clone $dynamicFilter;
$duplicate->addDynamicFilter($duplicateFilter);
$duplicateFilter->setSegment($duplicate);
$entityManager->persist($duplicateFilter);
}
$entityManager->persist($duplicate);
$entityManager->flush();
});
return $duplicate;
}
} }

View File

@ -112,6 +112,39 @@ class SegmentSaveControllerTest extends \MailPoetTest {
$this->saveController->save($segmentData); $this->saveController->save($segmentData);
} }
public function testItCanDuplicateExistingSegment(): void {
$segment = $this->createSegment('original');
$this->addDynamicFilter($segment, ['administrator']);
$this->addDynamicFilter($segment, ['editor']);
$duplicate = $this->saveController->duplicate($segment);
expect($duplicate->getId())->notEquals($segment->getId());
$filters = $duplicate->getDynamicFilters();
expect($filters)->count(2);
$originalFilter1 = $segment->getDynamicFilters()->get(0);
$duplicateFilter1 = $duplicate->getDynamicFilters()->get(0);
$this->assertInstanceOf(DynamicSegmentFilterEntity::class, $originalFilter1);
$this->assertInstanceOf(DynamicSegmentFilterEntity::class, $duplicateFilter1);
expect($originalFilter1->getId())->notEquals($duplicateFilter1->getId());
expect($duplicateFilter1->getFilterData()->getAction())->equals(UserRole::TYPE);
expect($duplicateFilter1->getFilterData()->getParam('wordpressRole'))->equals(['administrator']);
expect($duplicateFilter1->getFilterData()->getParam('connect'))->equals(DynamicSegmentFilterData::CONNECT_TYPE_AND);
$originalFilter2 = $segment->getDynamicFilters()->get(1);
$duplicateFilter2 = $duplicate->getDynamicFilters()->get(1);
$this->assertInstanceOf(DynamicSegmentFilterEntity::class, $originalFilter2);
$this->assertInstanceOf(DynamicSegmentFilterEntity::class, $duplicateFilter2);
expect($originalFilter2->getId())->notEquals($duplicateFilter2->getId());
expect($duplicateFilter2->getFilterData()->getAction())->equals(UserRole::TYPE);
expect($duplicateFilter2->getFilterData()->getParam('wordpressRole'))->equals(['editor']);
expect($duplicateFilter2->getFilterData()->getParam('connect'))->equals(DynamicSegmentFilterData::CONNECT_TYPE_AND);
}
private function createSegment(string $name): SegmentEntity { private function createSegment(string $name): SegmentEntity {
$segment = new SegmentEntity($name, SegmentEntity::TYPE_DYNAMIC, 'description'); $segment = new SegmentEntity($name, SegmentEntity::TYPE_DYNAMIC, 'description');
$this->entityManager->persist($segment); $this->entityManager->persist($segment);

View File

@ -64,6 +64,7 @@
'multipleSegmentsRestored': __('%1$d lists have been restored from the Trash.'), 'multipleSegmentsRestored': __('%1$d lists have been restored from the Trash.'),
'duplicate': __('Duplicate'), 'duplicate': __('Duplicate'),
'listDuplicated': __('List "%1$s" has been duplicated.'), 'listDuplicated': __('List "%1$s" has been duplicated.'),
'segmentDuplicated': __('Segment "%1$s" has been duplicated.'),
'update': __('Update'), 'update': __('Update'),
'forceSync': __('Force Sync'), 'forceSync': __('Force Sync'),
'readMore': __('Read More'), 'readMore': __('Read More'),