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

Feature/979 copy field values #12436

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ccaa6f3
POC for copying fields over (WIP)
i-just Nov 30, 2022
e51cac0
show notifications
i-just Dec 1, 2022
7dc40cf
copy only top level fields
i-just Dec 1, 2022
3f2ce0c
site select hooked up
i-just Dec 1, 2022
bfe9ff4
additional safeguards; adjust for title, slug; bug fixes
i-just Dec 2, 2022
5784fa7
Merge branch 'develop' into feature/979-copy-field-values-from-other-…
i-just Dec 2, 2022
2456d85
Merge branch '4.4' into feature/979-copy-field-values-from-other-sites
i-just Dec 2, 2022
74b215e
bug fix
i-just Dec 2, 2022
870a6a5
copyable field interface added
i-just Dec 7, 2022
d922efe
copy all element content; improve checks
i-just Dec 7, 2022
f72bddc
bug fix and refactoring
i-just Dec 8, 2022
d33c140
Merge branch 'develop' into feature/979-copy-field-values-v1
i-just Dec 9, 2022
a5229ab
disclosure menu for selecting site to copy from
i-just Dec 9, 2022
d8f3fae
styling improvement
i-just Dec 12, 2022
97910e2
Merge branch '4.4' into feature/979-copy-field-values-v1
brandonkelly Jan 11, 2023
7db371e
Merge branch '4.4' into feature/979-copy-field-values-v1
i-just Feb 20, 2023
6436864
make translatable icon accessible via keyboard
i-just Feb 20, 2023
53454b8
Merge remote-tracking branch 'origin/4.5' into feature/979-copy-field…
i-just Apr 12, 2023
06575f5
Merge branch '4.5' into feature/979-copy-field-values-v1
i-just Jul 20, 2023
c85950a
UI amends
i-just Jul 20, 2023
b83ac89
Merge branch '5.0' into feature/979-copy-field-values-v1
i-just Oct 13, 2023
aa6caac
support for multi-instance fields
i-just Oct 13, 2023
939654f
WIP - close the hud and reload the page
i-just Oct 13, 2023
4512263
bug fixes
i-just Oct 16, 2023
761924f
Merge branch '5.0' into feature/979-copy-field-values-v1
i-just Oct 16, 2023
38aaf79
tweaks
i-just Oct 17, 2023
fee2823
Merge branch '5.0' of github.com:craftcms/cms into feature/979-copy-f…
brianjhanson Dec 11, 2023
71397bc
Merge branch '5.0' into feature/979-copy-field-values-v1
i-just Dec 12, 2023
282dc8b
copyable country field
i-just Dec 12, 2023
5fd7d2f
improved slideout handling + notices
i-just Dec 12, 2023
7f3a723
Remove "Copy Content"
brianjhanson Dec 12, 2023
a6ec94f
Moving things around (ugly but works)
brianjhanson Dec 12, 2023
98db824
Move HUD into modal
brianjhanson Dec 13, 2023
3d8ddbf
Cleanup modal a bit
brianjhanson Dec 13, 2023
fb9e513
Merge branch '5.0' of github.com:craftcms/cms into feature/979-copy-f…
brianjhanson Dec 13, 2023
ab52b6e
Tooltip UI
brianjhanson Dec 13, 2023
b3a4700
Fix duplicate draft issue
brianjhanson Dec 13, 2023
646413a
Fix required field error
brianjhanson Dec 14, 2023
5079124
Merge branch '5.0' of github.com:craftcms/cms into feature/979-copy-f…
brianjhanson Dec 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/base/CopyableFieldInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\base;

/**
* CopyableFieldInterface defines the common interface to be implemented by field classes
* that wish to support copying their values between sites in a multisite installation.
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 4.4.0
*/
interface CopyableFieldInterface
{
/**
* Returns whether the field is copyable between sites.
*
* @param ElementInterface|null $element
* @return bool
*/
public function getIsCopyable(?ElementInterface $element = null): bool;

/**
* Copies field’s value from one element to another.
*
* @param ElementInterface $from
* @param ElementInterface $to
* @return bool
* @since 5.0.0
*/
public function copyValueBetweenSites(ElementInterface $from, ElementInterface $to): bool;
}
13 changes: 13 additions & 0 deletions src/base/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -3477,6 +3477,19 @@ protected function safeActionMenuItems(): array
];
}

if (ElementHelper::supportsFieldCopying($this)) {
$copyId = sprintf('action-copy-content-%s', mt_rand());
$items[] = [
'id' => $copyId,
'icon' => 'world',
'label' => Craft::t('app', 'Copy content from site'),
'type' => MenuItemType::Button,
'attributes' => [
'class' => ['copy-all-from-site'],
],
];
}

// Edit
if (Craft::$app->getElements()->canView($this)) {
$editId = sprintf('action-edit-%s', mt_rand());
Expand Down
26 changes: 25 additions & 1 deletion src/base/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,30 @@ public function getTranslationKey(ElementInterface $element): string
return ElementHelper::translationKey($element, $this->translationMethod, $this->translationKeyFormat);
}

/**
* Returns whether the field is copyable between sites.
*/
public function getIsCopyable(?ElementInterface $element = null): bool
{
return false;
}

/**
* @inheritdoc
*/
public function copyValueBetweenSites(ElementInterface $from, ElementInterface $to): bool
{
$fromValue = $this->serializeValue($from->getFieldValue($this->handle), $from);
$toValue = $this->serializeValue($to->getFieldValue($this->handle), $to);

if ($fromValue != $toValue) {
$to->setFieldValue($this->handle, $fromValue);
return true;
}

return false;
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -719,7 +743,7 @@ public function serializeValue(mixed $value, ?ElementInterface $element): mixed
}

/**
* @inheritdoc
* Copies field’s value from one element to another.
*/
public function copyValue(ElementInterface $from, ElementInterface $to): void
{
Expand Down
125 changes: 125 additions & 0 deletions src/controllers/ElementsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
use craft\i18n\Locale;
use craft\models\ElementActivity;
use craft\models\FieldLayoutForm;
use craft\models\Site;
use craft\web\Controller;
use craft\web\CpScreenResponseBehavior;
use craft\web\View;
use Illuminate\Support\Collection;
use Throwable;
use yii\helpers\Markdown;
use yii\web\BadRequestHttpException;
Expand Down Expand Up @@ -527,6 +529,129 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null):
return $response;
}

/**
* Returns result of copying field value from another site
*
* @return Response
* @throws BadRequestHttpException
* @throws ForbiddenHttpException
* @throws ServerErrorHttpException
* @throws Throwable
* @throws \craft\errors\ElementNotFoundException
* @throws \craft\errors\InvalidFieldException
* @throws \craft\errors\MissingComponentException
* @throws \yii\base\Exception
* @throws \yii\db\Exception
*/
public function actionCopyFieldValuesFromSite(): Response
{
$this->requireCpRequest();

/** @var Element|null $element */
$element = $this->_element();

if (!$element || $element->getIsRevision()) {
throw new BadRequestHttpException('No element was identified by the request.');
}

// if $fieldHandle is null, we're copying all element fields
$fieldHandle = $this->request->getBodyParam('copyFieldHandle', null);
$copyFromSiteId = $this->request->getRequiredBodyParam('copyFromSiteId');
$isFullPage = $this->request->getBodyParam('isFullPage');

if ($fieldHandle === '' || empty($copyFromSiteId)) {
throw new BadRequestHttpException("Request missing required param");
}

$elementsService = Craft::$app->getElements();
$user = static::currentUser();

// if we can't create drafts for this element, just bail;
// this check is to both check if user can create drafts AND if element supports them
if (!$elementsService->canCreateDrafts($element, $user)) {
throw new ForbiddenHttpException('Can‘t create drafts for this element.');
}

// check if this entry exists for other sites
if (empty($siteIdsForElement = $elementsService->getEnabledSiteIdsForElement($element->id))) {
$errorMsg = Craft::t('app', 'Couldn’t find this {type} on other sites.', [
'type' => $element::lowerDisplayName(),
]);

Craft::$app->session->setError($errorMsg);

return $this->_asFailure($element, $errorMsg);
}

if (!in_array($copyFromSiteId, $siteIdsForElement, false)) {
$errorMsg = Craft::t('app', 'Couldn’t find this {type} on the site you selected.', [
'type' => $element::lowerDisplayName(),
]);

Craft::$app->session->setError($errorMsg);

return $this->_asFailure($element, $errorMsg);
}

$result = $elementsService->copyFieldValuesFromSite($element, $fieldHandle, $copyFromSiteId);
if ($result['success'] === false) {
if ($isFullPage) {
Craft::$app->session->setError($result['message']);
}

return $this->_asFailure($result['element'], $result['message']);
}

if (!$this->request->getAcceptsJson()) {
// Tell all browser windows about the element save
Craft::$app->getSession()->broadcastToJs([
'event' => 'saveElement',
'id' => $element->id,
]);
}

if ($isFullPage) {
Craft::$app->session->setNotice($result['message']);
}

return $this->_asSuccess($result['message'], $result['element']);
}

public function actionCopyFromSiteForm(): Response
{
$this->requireAcceptsJson();
$this->requireCpRequest();

$viewMode = Craft::$app->getRequest()->getParam('viewMode', 'modal');
$copyFieldHandle = Craft::$app->getRequest()->getParam('copyFieldHandle');
$template = $viewMode === 'modal' ? '_special/copy-content-modal.twig' : '_special/copy-content-fields.twig';

$element = $this->_element();
$sitesService = Craft::$app->getSites();

$siteOptions = Collection::make(ElementHelper::editableSiteIdsForElement($element))
->filter(fn(int $siteId) => $siteId !== $this->_siteId)
->map(fn(int $siteId) => $sitesService->getSiteById($siteId))
->map(fn(Site $site) => [
'label' => $site->name,
'value' => $site->id,
]);

$view = $this->getView();
$html = $view->renderTemplate($template, [
'viewMode' => $viewMode,
'siteOptions' => $siteOptions,
'copyFieldHandle' => $copyFieldHandle,
'elementId' => $element->id,
'draftId' => $this->_draftId,
'provisional' => $this->_provisional,
]);

return $this->asJson([
'html' => $html,
]);
}

/**
* Returns an element revisions index screen.
*
Expand Down
13 changes: 13 additions & 0 deletions src/fieldlayoutelements/BaseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ public function formHtml(?ElementInterface $element = null, bool $static = false
'orientation' => $this->orientation($element, $static),
'translatable' => $this->translatable($element, $static),
'translationDescription' => $this->translationDescription($element, $static),
'copyable' => $this->isCopyable($element, $static),
'errors' => !$static ? $this->errors($element) : [],
]);
}
Expand Down Expand Up @@ -686,4 +687,16 @@ protected function translationDescription(?ElementInterface $element = null, boo
{
return null;
}

/**
* Returns whether field supports copying its value across sites
*
* @param ElementInterface|null $element
* @param bool $static
* @return bool
*/
public function isCopyable(?ElementInterface $element = null, bool $static = false): bool
{
return false;
}
}
13 changes: 13 additions & 0 deletions src/fieldlayoutelements/CustomField.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace craft\fieldlayoutelements;

use Craft;
use craft\base\CopyableFieldInterface;
use craft\base\ElementInterface;
use craft\base\FieldInterface;
use craft\base\PreviewableFieldInterface;
Expand Down Expand Up @@ -378,4 +379,16 @@ protected function translationDescription(?ElementInterface $element = null, boo
{
return $this->_field->getTranslationDescription($element);
}

/**
* @inheritdoc
*/
public function isCopyable(?ElementInterface $element = null, bool $static = false): bool
{
if ($this->_field instanceof CopyableFieldInterface) {
return $this->_field->getIsCopyable($element);
}

return false;
}
}
8 changes: 8 additions & 0 deletions src/fieldlayoutelements/entries/EntryTitleField.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,12 @@ public function inputHtml(?ElementInterface $element = null, bool $static = fals

return parent::inputHtml($element, $static);
}

/**
* @inheritdoc
*/
public function isCopyable(?ElementInterface $element = null, bool $static = false): bool
{
return $this->translatable($element) && ElementHelper::supportsFieldCopying($element);
}
}
12 changes: 11 additions & 1 deletion src/fields/BaseOptionsField.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace craft\fields;

use Craft;
use craft\base\CopyableFieldInterface;
use craft\base\ElementInterface;
use craft\base\Field;
use craft\base\PreviewableFieldInterface;
Expand All @@ -21,6 +22,7 @@
use craft\gql\resolvers\OptionField as OptionFieldResolver;
use craft\helpers\ArrayHelper;
use craft\helpers\Cp;
use craft\helpers\ElementHelper;
use craft\helpers\Json;
use craft\helpers\StringHelper;
use GraphQL\Type\Definition\Type;
Expand All @@ -32,7 +34,7 @@
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0.0
*/
abstract class BaseOptionsField extends Field implements PreviewableFieldInterface
abstract class BaseOptionsField extends Field implements PreviewableFieldInterface, CopyableFieldInterface
{
/**
* @event DefineInputOptionsEvent Event triggered when defining the options for the field's input.
Expand Down Expand Up @@ -504,6 +506,14 @@ public function getContentGqlMutationArgumentType(): Type|array
];
}

/**
* @inheritdoc
*/
public function getIsCopyable(?ElementInterface $element = null): bool
{
return $this->getIsTranslatable($element) && ElementHelper::supportsFieldCopying($element);
}

/**
* Returns the label for the Options setting.
*
Expand Down
11 changes: 10 additions & 1 deletion src/fields/BaseRelationField.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Craft;
use craft\base\conditions\ConditionInterface;
use craft\base\CopyableFieldInterface;
use craft\base\EagerLoadingFieldInterface;
use craft\base\Element;
use craft\base\ElementInterface;
Expand Down Expand Up @@ -50,7 +51,7 @@
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0.0
*/
abstract class BaseRelationField extends Field implements InlineEditableFieldInterface, EagerLoadingFieldInterface
abstract class BaseRelationField extends Field implements InlineEditableFieldInterface, EagerLoadingFieldInterface, CopyableFieldInterface
{
/**
* @event ElementCriteriaEvent The event that is triggered when defining the selection criteria for this field.
Expand Down Expand Up @@ -918,6 +919,14 @@ public function getContentGqlMutationArgumentType(): Type|array
];
}

/**
* @inheritdoc
*/
public function getIsCopyable(?ElementInterface $element = null): bool
{
return $this->getIsTranslatable($element) && ElementHelper::supportsFieldCopying($element);
}

// Events
// -------------------------------------------------------------------------

Expand Down
Loading