Skip to content

Commit

Permalink
Entry/category (deep-)duplication
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonkelly committed Nov 6, 2018
1 parent 2c7c92a commit 8216c33
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/elements/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions src/elements/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) &&
Expand Down
32 changes: 32 additions & 0 deletions src/elements/actions/DeepDuplicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\elements\actions;

use Craft;
use craft\base\Element;
use craft\base\ElementAction;
use craft\base\ElementInterface;
use craft\elements\db\ElementQueryInterface;
use craft\helpers\ArrayHelper;

/**
* DeepDuplicate represents a "Duplicate (with descendants)" element action.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 3.0.30
*/
class DeepDuplicate extends Duplicate
{
// Properties
// =========================================================================

/**
* @inheritdoc
*/
public $deep = true;
}
135 changes: 135 additions & 0 deletions src/elements/actions/Duplicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\elements\actions;

use Craft;
use craft\base\Element;
use craft\base\ElementAction;
use craft\base\ElementInterface;
use craft\elements\db\ElementQueryInterface;
use craft\helpers\ArrayHelper;

/**
* Duplicate represents a Duplicate element action.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @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);
}
}
}
}

0 comments on commit 8216c33

Please sign in to comment.