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

Copy field values from other sites #14056

Open
wants to merge 107 commits into
base: 5.6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 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
52b1191
Merge branch '5.0' of github.com:craftcms/cms into feature/cms-979-co…
brianjhanson Dec 18, 2023
bc630e1
Merge commit '5fd7d2f5a3082b013b7cab7693bb95c9c584ca3b' of github.com…
brianjhanson Dec 18, 2023
3c2a490
Send back HTML when updating fields
brianjhanson Dec 19, 2023
278b089
Move field translation icon into button
brianjhanson Dec 19, 2023
e07ac12
Fix bug coping reserved handles
brianjhanson Dec 19, 2023
7252f3a
Convert copy attribute to web component
brianjhanson Dec 19, 2023
e44182f
Convert copy attribute to web component
brianjhanson Dec 19, 2023
48350bb
Better HUD hiding
brianjhanson Dec 19, 2023
7c9a0d5
Add `data-layout-element` attribute
brianjhanson Dec 19, 2023
1a7ca0e
Give up on Matrix for the moment
brianjhanson Dec 19, 2023
32dc390
More explicit namespace setting
brianjhanson Dec 19, 2023
0d95d47
Use event delegation
brianjhanson Dec 19, 2023
7d42d4e
Copy Matrix fields
brianjhanson Dec 20, 2023
2a8c259
Build
brianjhanson Dec 20, 2023
ac26858
Merge branch '5.5' of github.com:craftcms/cms into feature/cms-979-co…
brianjhanson Sep 24, 2024
cc30dec
fix saving issue that occurred after merge
i-just Sep 25, 2024
8a67e08
comments, since tags and remove repetition
i-just Sep 25, 2024
b6d1786
copyable support for Icon and Link fields
i-just Sep 25, 2024
6e434a6
cleanup
i-just Sep 25, 2024
1df74bb
allow copying for elements that don't support drafts
i-just Sep 25, 2024
3bfb7e0
better way to handle not custom fields
i-just Sep 25, 2024
4c706f2
native alt field is translatable, so allow copying
i-just Sep 25, 2024
d6f3d84
namespace inputs differently
i-just Sep 25, 2024
c7de1e7
build
i-just Sep 25, 2024
7dba512
Wrap translation icons in `craft-tooltip`s
brianjhanson Sep 26, 2024
a24c1f7
Fix z-index
brianjhanson Sep 26, 2024
f6a57f0
Add ability to trigger tooltip from outside element
brianjhanson Sep 26, 2024
c9ad2ff
Remove `aria-label` reference
brianjhanson Sep 26, 2024
14d1e38
Minor update
brianjhanson Sep 26, 2024
426d342
Better auto-updating
brianjhanson Sep 26, 2024
95e71d1
Actually remove event listner
brianjhanson Sep 26, 2024
0dc8ff2
permissions & static context
i-just Sep 27, 2024
e3fddac
don't show copy action in chips/cards
i-just Sep 27, 2024
cad0f64
build
i-just Sep 27, 2024
583a039
Merge branch '5.5' into feature/cms-979-copy-field-values-from-other-…
i-just Sep 27, 2024
6d91d54
disallow copying for fields in matrix "blocks"
i-just Sep 27, 2024
d586e72
Remove need to `.bind(this)`
brianjhanson Sep 27, 2024
027d36e
Increase delay a bit
brianjhanson Sep 27, 2024
7f99d21
allow copying of fields in matrix "block" but only when opened as a s…
i-just Oct 1, 2024
c823223
Copy individual inline-matrix fields
brianjhanson Oct 3, 2024
9d24f6b
Remove copy blocking
brianjhanson Oct 3, 2024
e1c041c
Cleanup console.log
brianjhanson Oct 3, 2024
b0758a1
Merge branch '5.5' into feature/cms-979-copy-field-values-from-other-…
i-just Oct 7, 2024
26cb281
namespace adjustment - fixes modified field issue (and others)
i-just Oct 7, 2024
dbe0c90
bug fix - copying matrix field content when the matrix is "translatable"
i-just Oct 7, 2024
536123a
build
i-just Oct 7, 2024
fde01db
Revert "namespace adjustment - fixes modified field issue (and others)"
i-just Oct 7, 2024
b16b8bb
marry matrix "blocks" and "entries" copying
i-just Oct 7, 2024
b8c9c6d
build
i-just Oct 7, 2024
7af7fb2
empty value is still a value
i-just Oct 10, 2024
fb60503
ensure draft before copying the entire element too
i-just Oct 10, 2024
ef78914
build
i-just Oct 10, 2024
ab4dd6a
Merge branch '5.5' into feature/cms-979-copy-field-values-from-other-…
i-just Oct 10, 2024
dea7ea6
back to Brian's namespacing, but with a twist
i-just Oct 11, 2024
bb3fda6
build
i-just Oct 11, 2024
4435084
more namespacing changes
i-just Oct 11, 2024
43e98e3
Merge branch '5.5' into feature/cms-979-copy-field-values-from-other-…
i-just Oct 17, 2024
ac4dccf
Merge branch '5.5' into feature/cms-979-copy-field-values-from-other-…
i-just Oct 23, 2024
796f459
fixes for copy all accessibility issues
i-just Oct 23, 2024
9b498d8
change aria-role to role
i-just Oct 23, 2024
681948c
trigger field copy dialogue via keyboard
i-just Oct 23, 2024
77b20a6
build
i-just Oct 23, 2024
28fabc4
only create provisional draft first if we're about to copy field with…
i-just Oct 23, 2024
79a633f
build
i-just Oct 23, 2024
464f6ed
announce loading/complete for filter hud
i-just Oct 24, 2024
f0580af
Move copy button to right side of field
brianjhanson Oct 24, 2024
78741c6
Handle `aria-expanded` for HUD
brianjhanson Oct 24, 2024
705f088
Add modal heading
brianjhanson Oct 24, 2024
cc1c6de
Add toggle via click
brianjhanson Oct 24, 2024
7a44349
Remove self-managed
brianjhanson Oct 24, 2024
2c65a82
Meaningful label
brianjhanson Oct 24, 2024
d99cbb5
Build
brianjhanson Oct 25, 2024
a7437b5
accessibility amends round 2
i-just Oct 25, 2024
b791847
don't show the icon if there's no sites to copy from & performance
i-just Oct 25, 2024
2d92601
Merge branch '5.5' into feature/cms-979-copy-field-values-from-other-…
i-just Oct 25, 2024
6b25bff
Keep tooltip open when hovering
brianjhanson Nov 1, 2024
c2b50d6
Merge branch '5.5' of github.com:craftcms/cms into feature/cms-979-co…
brianjhanson Nov 4, 2024
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
4 changes: 2 additions & 2 deletions packages/craftcms-sass/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ $menuBorderRadius: $mediumBorderRadius;
sans-serif;
}

@mixin fixed-width-font {
@mixin fixed-width-font($size: 0.9em) {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
font-size: 0.9em !important;
font-size: $size !important;
}

@function toRem($values...) {
Expand Down
10 changes: 10 additions & 0 deletions packages/craftcms-webpack/Craft.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,24 @@ interface ProgressBarInterface {
showProgressBar(): void;
}

type Site = {
handle: string;
id: number;
name: string;
uid: string;
};

// Declare existing variables, mock the things we'll use.
declare var Craft: {
csrfTokenName?: string;
csrfTokenValue?: string;
ProgressBar: ProgressBarInterface;
t(category: string, message: string, params?: object): string;
sendActionRequest(method: string, action: string, options?: object): Promise;
initUiElements($container: JQuery): void;
expandPostArray(arr: object): any;
escapeHtml(str: string);
sites: Site[];
Preview: any;
cp: any;
};
Expand Down
35 changes: 35 additions & 0 deletions src/base/CopyableFieldInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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. <[email protected]>
* @since 5.5.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
*/
public function copyValueBetweenSites(ElementInterface $from, ElementInterface $to): bool;
}
53 changes: 52 additions & 1 deletion src/base/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use craft\elements\User;
use craft\enums\AttributeStatus;
use craft\enums\Color;
use craft\enums\MenuItemType;
use craft\errors\InvalidFieldException;
use craft\events\AuthorizationCheckEvent;
use craft\events\DefineAttributeHtmlEvent;
Expand Down Expand Up @@ -2245,6 +2246,12 @@ private static function _indexOrderByColumns(
*/
private $_serializeFields = false;

/**
* @var bool|null
* @see getIsCopyable()
*/
private ?bool $_isCopyable = null;

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -3605,6 +3612,31 @@ protected function safeActionMenuItems(): array
];
}

// Copy content
$user = Craft::$app->getUser()->getIdentity();
if (
!$this->getIsRevision() &&
$this->canSave($user) &&
$this->getIsCopyable()
) {
$copyContentId = sprintf('action-copy-content-%s', mt_rand());
$items[] = [
'id' => $copyContentId,
'icon' => 'clone',
'label' => Craft::t('app', 'Copy content from site'),
'type' => MenuItemType::Button,
'showInChips' => false,
'attributes' => [
'data' => [
'copy-content' => true,
],
'aria' => [
'label' => Craft::t('app', 'Copy content from site'),
],
],
];
}

// Edit
if (Craft::$app->getElements()->canView($this)) {
$editId = sprintf('action-edit-%s', mt_rand());
Expand Down Expand Up @@ -5219,6 +5251,25 @@ public function getCurrentRevision(): ?ElementInterface
return $this->_currentRevision ?: null;
}

/**
* @inheritdoc
*/
public function getIsCopyable(): bool
{
if (!isset($this->_isCopyable)) {
$this->_isCopyable = !(
!Craft::$app->getIsMultiSite() ||
// check if user can edit this element in other site ids
count(ElementHelper::editableSiteIdsForElement($this)) < 2 ||
// also check if the element exists in other sites
empty(array_diff(array_keys(ElementHelper::siteStatusesForElement($this, true)), [$this->siteId]))
);
;
}

return $this->_isCopyable;
}

// Indexes, etc.
// -------------------------------------------------------------------------

Expand Down Expand Up @@ -5857,7 +5908,7 @@ public function beforeSave(bool $isNew): bool
$this->trigger(self::EVENT_BEFORE_SAVE, $event);
return $event->isValid;
}

return true;
}

Expand Down
10 changes: 10 additions & 0 deletions src/base/ElementInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,16 @@ public function setRevisionNotes(?string $notes): void;
*/
public function getCurrentRevision(): ?self;

/**
* Return if the element is copyable between sites.
* Checks if it's a multisite installation, if user can edit the element in other sites,
* and if the element actually exists in other sites.
*
* @return bool
* @since 5.5.0
*/
public function getIsCopyable(): bool;

// Indexes, etc.
// -------------------------------------------------------------------------

Expand Down
6 changes: 6 additions & 0 deletions src/base/ElementTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,10 @@ trait ElementTrait
* @since 3.2.0
*/
public bool $hardDelete = false;

/**
* @var bool Whether the element is having its values copied across sites.
* @since 5.5.0
*/
public bool $copying = false;
}
37 changes: 36 additions & 1 deletion src/base/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,41 @@ public function getTranslationKey(ElementInterface $element): string
return ElementHelper::translationKey($element, $this->translationMethod, $this->translationKeyFormat);
}

/**
* @see CopyableFieldInterface::getIsCopyable()
* @since 5.5.0
*/
public function getIsCopyable(?ElementInterface $element = null): bool
{
return $this->getIsTranslatable($element) && $element?->getIsCopyable();
}

/**
* @see CopyableFieldInterface::copyValueBetweenSites()
* @since 5.5.0
*/
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;
}

/**
* Returns whether field contains nested elements, and uses the nested element manager.
* @since 5.5.0
*/
public function getIsNested(?ElementInterface $element = null): bool
{
return $this instanceof ElementContainerFieldInterface;
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -919,7 +954,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
104 changes: 104 additions & 0 deletions src/controllers/ElementsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,110 @@ 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
* @since 5.5.0
*/
public function actionCopyFieldValuesFromSite(): Response
{
$this->requireCpRequest();

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

if ($element instanceof Response) {
return $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('fieldHandle', null);
$copyFromSiteId = $this->request->getRequiredBodyParam('copyFromSiteId');
$namespace = $this->request->getBodyParam('namespace');

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

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

// 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(),
]);

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

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

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

// Now we can actually copy things
$updates = $elementsService->copyFieldValuesFromSite($element, $fieldHandle, $copyFromSiteId);
if (count(array_keys($updates)) === 0) {
return $this->_asSuccess(Craft::t('app', 'Nothing to copy.'), $element, [
'fragments' => [],
]);
}

$fragments = [];
$view = Craft::$app->getView();
$layout = $element->getFieldLayout();

// Loop over each of the updated fields and gather the HTML of the field
foreach ($layout->getTabs() as $tab) {
foreach ($tab->getElements() as $layoutElement) {
if ($layoutElement instanceof BaseField) {
$attribute = $layoutElement->attribute();

// Only return attributes that were updated
if (in_array($attribute, array_keys($updates))) {
$html = $view->namespaceInputs(function() use ($element, $layoutElement) {
return $layoutElement->formHtml($element);
}, $namespace); // you have to pass the $namespace here or some attrs won't get properly namespaced (e.g. title)

if ($html) {
$html = Html::modifyTagAttributes($html, [
'data' => [
'layout-element' => $layoutElement->uid,
],
]);
}

$fragments[$layoutElement->uid] = $html;
}
}
}
}

return $this->_asSuccess('Content copied.', $element, [
'headHtml' => $view->getHeadHtml(),
'bodyHtml' => $view->getBodyHtml(),
'fragments' => $fragments,
]);
}

/**
* Returns an element revisions index screen.
*
Expand Down
6 changes: 4 additions & 2 deletions src/elements/NestedElementManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,9 @@ public function maintainNestedElements(ElementInterface $owner, bool $isNew): vo
{
$resetValue = false;

if ($owner->duplicateOf !== null) {
if ($owner->copying) {
$this->duplicateNestedElements($owner->duplicateOf, $owner, true, !$isNew, true);
} elseif ($owner->duplicateOf !== null) {
// If this is a draft, its nested element ownership will be duplicated by Drafts::createDraft()
if ($owner->getIsRevision()) {
$this->createRevisions($owner->duplicateOf, $owner);
Expand Down Expand Up @@ -956,7 +958,7 @@ private function duplicateNestedElements(

$transaction = Craft::$app->getDb()->beginTransaction();
try {
$setCanonicalId = $target->getIsDerivative() && $target->getCanonical()->id !== $target->id;
$setCanonicalId = !$target->copying && $target->getIsDerivative() && $target->getCanonical()->id !== $target->id;

/** @var NestedElementInterface[] $elements */
foreach ($elements as $element) {
Expand Down
Loading
Loading