From 8216c3370d50bf4e881024a1524bef52918b0a33 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Tue, 6 Nov 2018 11:41:51 -0800 Subject: [PATCH] Entry/category (deep-)duplication --- CHANGELOG-v3.md | 3 + src/elements/Category.php | 9 ++ src/elements/Entry.php | 11 ++ src/elements/actions/DeepDuplicate.php | 32 ++++++ src/elements/actions/Duplicate.php | 135 +++++++++++++++++++++++++ 5 files changed, 190 insertions(+) create mode 100644 src/elements/actions/DeepDuplicate.php create mode 100644 src/elements/actions/Duplicate.php diff --git a/CHANGELOG-v3.md b/CHANGELOG-v3.md index d4604e9bda8..eba07b19b9c 100644 --- a/CHANGELOG-v3.md +++ b/CHANGELOG-v3.md @@ -3,7 +3,10 @@ ## Unreleased ### Added +- Added “Duplicate” and “Duplicate (with children)” actions to the Entries and Categories index pages. ([#1291](https://github.com/craftcms/cms/issues/1291)) - Added `craft\base\ElementAction::$elementType`, which element action classes can use to reference their associated element type. +- Added `craft\elements\actions\DeepDuplicate`. +- Added `craft\elements\actions\Duplicate`. - Added `craft\elements\actions\SetStatus::$allowDisabledForSite`, which can be used by localizable element types to enable a “Disabled for Site” status option. ### Changed diff --git a/src/elements/Category.php b/src/elements/Category.php index 9ad075e8f18..f6e089598f4 100644 --- a/src/elements/Category.php +++ b/src/elements/Category.php @@ -11,7 +11,9 @@ use craft\base\Element; use craft\controllers\ElementIndexesController; use craft\db\Query; +use craft\elements\actions\DeepDuplicate; use craft\elements\actions\Delete; +use craft\elements\actions\Duplicate; use craft\elements\actions\Edit; use craft\elements\actions\NewChild; use craft\elements\actions\SetStatus; @@ -176,6 +178,13 @@ protected static function defineActions(string $source = null): array ]); } + // Duplicate + $actions[] = Duplicate::class; + + if ($group->maxLevels != 1) { + $actions[] = DeepDuplicate::class; + } + // Delete $actions[] = Craft::$app->getElements()->createAction([ 'type' => Delete::class, diff --git a/src/elements/Entry.php b/src/elements/Entry.php index 95c35a44cbe..e69115a1579 100644 --- a/src/elements/Entry.php +++ b/src/elements/Entry.php @@ -11,7 +11,9 @@ use craft\base\Element; use craft\controllers\ElementIndexesController; use craft\db\Query; +use craft\elements\actions\DeepDuplicate; use craft\elements\actions\Delete; +use craft\elements\actions\Duplicate; use craft\elements\actions\Edit; use craft\elements\actions\NewChild; use craft\elements\actions\SetStatus; @@ -328,6 +330,15 @@ protected static function defineActions(string $source = null): array } } + // Duplicate + if ($userSessionService->checkPermission('publishEntries:' . $section->id)) { + $actions[] = Duplicate::class; + + if ($section->type === Section::TYPE_STRUCTURE && $section->maxLevels != 1) { + $actions[] = DeepDuplicate::class; + } + } + // Delete? if ( $userSessionService->checkPermission('deleteEntries:' . $section->id) && diff --git a/src/elements/actions/DeepDuplicate.php b/src/elements/actions/DeepDuplicate.php new file mode 100644 index 00000000000..be48426d64c --- /dev/null +++ b/src/elements/actions/DeepDuplicate.php @@ -0,0 +1,32 @@ + + * @since 3.0.30 + */ +class DeepDuplicate extends Duplicate +{ + // Properties + // ========================================================================= + + /** + * @inheritdoc + */ + public $deep = true; +} diff --git a/src/elements/actions/Duplicate.php b/src/elements/actions/Duplicate.php new file mode 100644 index 00000000000..a2155be739b --- /dev/null +++ b/src/elements/actions/Duplicate.php @@ -0,0 +1,135 @@ + + * @since 3.0.30 + */ +class Duplicate extends ElementAction +{ + // Properties + // ========================================================================= + + /** + * @var bool Whether to also duplicate the selected elements’ descendants + */ + public $deep = false; + + /** + * @var string|null The message that should be shown after the elements get deleted + */ + public $successMessage; + + // Public Methods + // ========================================================================= + + /** + * @inheritdoc + */ + public function getTriggerLabel(): string + { + return $this->deep + ? Craft::t('app', 'Duplicate (with descendants)') + : Craft::t('app', 'Duplicate'); + } + + // Public Methods + // ========================================================================= + + /** + * @inheritdoc + */ + public function performAction(ElementQueryInterface $query): bool + { + if ($this->deep) { + $query->orderBy(['structureelements.lft' => SORT_ASC]); + } + + /** @var Element[] $elements */ + $elements = $query->all(); + $successCount = 0; + $failCount = 0; + + $this->_duplicateElements($elements, $successCount, $failCount); + + // Did all of them fail? + if ($successCount === 0) { + $this->setMessage(Craft::t('app', 'Could not duplicate elements due to validation errors.')); + return false; + } + + if ($failCount !== 0) { + $this->setMessage(Craft::t('app', 'Could not duplicate all elements due to validation errors.')); + } else { + $this->setMessage(Craft::t('app', 'Elements duplicated.')); + } + + return true; + } + + /** + * @param Element[] $elements + * @param int[] $duplicatedElementIds + * @param int $successCount + * @param int $failCount + * @param ElementInterface|null $newParent + */ + private function _duplicateElements(array $elements, int &$successCount, int &$failCount, array &$duplicatedElementIds = [], ElementInterface $newParent = null) + { + $elementsService = Craft::$app->getElements(); + $structuresService = Craft::$app->getStructures(); + + foreach ($elements as $element) { + // Make sure this element wasn't already duplicated, which could + // happen if it's the descendant of a previously duplicated element + // and $this->deep == true. + if (isset($duplicatedElementIds[$element->id])) { + continue; + } + + $newAttributes = []; + if ($element::hasTitles()) { + $newAttributes['title'] = Craft::t('app', '{title} copy', ['title' => $element->title]); + } + + try { + $duplicate = $elementsService->duplicateElement($element, $newAttributes); + } catch (\Throwable $e) { + // Validation error + $failCount++; + continue; + } + + $successCount++; + $duplicatedElementIds[$element->id] = true; + + if ($newParent) { + // Append it to the duplicate of $element's parent + $structuresService->append($element->structureId, $duplicate, $newParent); + } else if ($element->structureId) { + // Place it right next to the original element + $structuresService->moveAfter($element->structureId, $duplicate, $element); + } + + if ($this->deep) { + $children = $element->getChildren()->anyStatus()->all(); + $this->_duplicateElements($children, $successCount, $failCount, $duplicatedElementIds, $duplicate); + } + } + } +}