From ccaa6f3699b17fa76cc452dca6d2366a556f839e Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 30 Nov 2022 17:25:02 +0000 Subject: [PATCH 01/26] POC for copying fields over (WIP) --- src/controllers/ElementsController.php | 61 +++++++++++++++ src/helpers/Cp.php | 4 +- src/services/Elements.php | 74 ++++++++++++++++++ src/web/assets/cp/src/js/ElementEditor.js | 94 +++++++++++++++++++++++ 4 files changed, 232 insertions(+), 1 deletion(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 895e0d50ae5..cce0d1bb10a 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -575,6 +575,67 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null): return $response; } + public function actionCopyFieldValueFromSite() + { + $this->requireAcceptsJson(); + + /** @var Element|null $element */ + $element = $this->_element(); + + if (!$element || $element->getIsRevision()) { + throw new BadRequestHttpException('No element was identified by the request.'); + } + + $params = $this->request->getBodyParams(); + $fieldHandle = $params['fieldHandle'] ?? null; + $copyFromSiteId = $params['copyFromSiteId'] ?? null; + + if ($fieldHandle === null || $copyFromSiteId === null) { + throw new BadRequestHttpException('No field handle or site id to copy from provided.'); + } + + $elementsService = Craft::$app->getElements(); + $user = static::currentUser(); + + // check if this entry exists for other sites + if (empty($siteIdsForElement = $elementsService->getEnabledSiteIdsForElement($element->id))) { + return $this->asFailure(Craft::t('app', 'Couldn\'t find this {type} in other sites.', [ + 'type' => $element::lowerDisplayName(), + ])); + } + + if (!in_array($copyFromSiteId, $siteIdsForElement, false)) { + return $this->asFailure(Craft::t('app', 'Couldn\'t find this {type} for the site you selected.', [ + 'type' => $element::lowerDisplayName(), + ])); + } + + // todo: IWONA check if we need those checks! + if (!$element->getIsDraft() && !$this->_provisional) { + if (!$elementsService->canCreateDrafts($element, $user)) { + throw new ForbiddenHttpException('User not authorized to create drafts for this element.'); + } + } elseif (!$this->_canSave($element, $user)) { + throw new ForbiddenHttpException('User not authorized to save this element.'); + } + + if (!$elementsService->copyFieldValueFromSite($element, $fieldHandle, $copyFromSiteId)) { + $this->_asFailure($element, Craft::t('app', 'Couldn\'t copy the value.')); + } + + if (!$this->request->getAcceptsJson()) { + // Tell all browser windows about the element save + Craft::$app->getSession()->broadcastToJs([ + 'event' => 'saveElement', + 'id' => $element->id, + ]); + } + + return $this->_asSuccess(Craft::t('app', 'Value copied.', [ + 'type' => $element::displayName(), + ]), $element); + } + /** * Returns the page title and document title that should be used for Edit Element pages. * diff --git a/src/helpers/Cp.php b/src/helpers/Cp.php index e6ab64a6525..9f40a50dfce 100644 --- a/src/helpers/Cp.php +++ b/src/helpers/Cp.php @@ -686,11 +686,13 @@ public static function fieldHtml(string $input, array $config = []): string 'title' => $config['translationDescription'] ?? Craft::t('app', 'This field is translatable.'), 'data' => [ 'icon' => 'language', + 'handle' => $config['id'], ], 'aria' => [ 'label' => $config['translationDescription'] ?? Craft::t('app', 'This field is translatable.'), ], - 'role' => 'img', + 'role' => 'link', + 'tabindex' => 0, ]) : '') ); diff --git a/src/services/Elements.php b/src/services/Elements.php index 8483c7de321..4d16f1e03bd 100644 --- a/src/services/Elements.php +++ b/src/services/Elements.php @@ -2143,6 +2143,80 @@ public function restoreElements(array $elements): bool return true; } + public function copyFieldValueFromSite(ElementInterface $element, string $fieldHandle, int $copyFromSiteId): array + { + // get element for selected site + /** @var string|ElementInterface $elementType */ + $elementType = get_class($element); + + $user = Craft::$app->getUser()->getIdentity(); + + $elementForSite = $this->getElementById($element->canonicalId, $elementType, $copyFromSiteId); + + if (!$elementForSite) { + return [ + 'success' => false, + 'message' => Craft::t('app', 'Couldn\'t find this {type} for the site you selected.', [ + 'type' => $element::lowerDisplayName(), + ]) + ]; + } + + $currentValue = $element->getFieldValue($fieldHandle); + $copyValue = $elementForSite->getFieldValue($fieldHandle); + + if (($field = Craft::$app->fields->getFieldByHandle($fieldHandle)) !== null) { + $currentValue = $field->serializeValue($currentValue); + $copyValue = $field->serializeValue($copyValue); + + if ($currentValue != $copyValue) { + $transaction = Craft::$app->getDb()->beginTransaction(); + try { + /*if ($value instanceof MatrixBlockQuery) { + $field = Craft::$app->fields->getFieldByHandle($fieldHandle); + $test = $field->serializeValue($value); + $elementClone = clone($element); + $elementClone->canonicalId = null; + Craft::$app->matrix->duplicateBlocks($field, $elementForSite, $elementClone, trackDuplications: false); + } else { + $element->setFieldValue($fieldHandle, $value); + }*/ + + // check if element is a draft - if not, create one + if (!$element->getIsDraft()) { + /** @var Element|DraftBehavior $element */ + $draft = Craft::$app->getDrafts()->createDraft($element, $user->id, null, null, [], true); + $draft->setCanonical($element); + $element = $draft; + } + + $element->setFieldValue($fieldHandle, $copyValue); + + if (!$this->saveElement($element, true, false, false)) { + return [ + 'success' => false, + 'message' => Craft::t('app', 'Couldn\'t copy the value.') + ]; + } + + $transaction->commit(); + return [ + 'success' => true, + 'message' => Craft::t('app', 'Value copied.') + ]; + } catch (Throwable $e) { + $transaction->rollBack(); + throw $e; + } + } + } + + return [ + 'success' => true, + 'message' => Craft::t('app', 'Nothing to copy.') + ]; + } + // Element classes // ------------------------------------------------------------------------- diff --git a/src/web/assets/cp/src/js/ElementEditor.js b/src/web/assets/cp/src/js/ElementEditor.js index 11f39989093..648a999e42c 100644 --- a/src/web/assets/cp/src/js/ElementEditor.js +++ b/src/web/assets/cp/src/js/ElementEditor.js @@ -98,6 +98,8 @@ Craft.ElementEditor = Garnish.Base.extend( this.$revisionLabel = this.$container.find('.revision-label'); this.$previewBtn = this.$container.find('.preview-btn'); + this.copyTranslationBtns = this.$container.find('.t9n-indicator'); + const $spinnerContainer = this.isFullPage ? $('#page-title') : this.slideout.$toolbar; @@ -121,6 +123,8 @@ Craft.ElementEditor = Garnish.Base.extend( 'click', 'expandSiteStatuses' ); + + this.addListener(this.copyTranslationBtns, 'click', 'showFieldTranslationDialogue'); } if (this.settings.previewTargets.length && this.isFullPage) { @@ -526,6 +530,96 @@ Craft.ElementEditor = Garnish.Base.extend( this._updateGlobalStatus(); }, + showFieldTranslationDialogue: function (ev) { + ev.preventDefault(); + + $btn = $(ev.target); + + let form = + '
' + + Craft.getCsrfInput() + + '' + + '' + + '' + + '' + + '
'; + + hud = new Garnish.HUD( + $btn, + `
` + + `` + + $btn.attr('title') + + `
` + + form + + `
` + ); + + this.addListener($('.copyTranslationForField'), 'submit', 'copyTranslatedValueFromSite'); + + /*var menuOptions = [{ + label: this.settings.siteId, + value: this.settings.siteId + }]; + this._getOtherSupportedSites().forEach((s) => + menuOptions.push({ + label: s.name, + value: s.id + }) + ); + console.log(menuOptions); + new Garnish.ContextMenu($("#copyTranslationSiteId"), menuOptions, {menuClass: 'menu'});*/ + }, + + copyTranslatedValueFromSite: function (ev) { + ev.preventDefault(); + let $form = $(ev.target); + + let params = { + fieldHandle: $form.find('[name="copyFieldHandle"]').val(), + copyFromSiteId: $form.find('[name="copyFromSiteId"]').val(), + elementId: this.settings.canonicalId, + draftId: this.settings.draftId, + provisional: this.settings.isProvisionalDraft, + } + + if (Craft.csrfTokenName) { + params[Craft.csrfTokenName] = Craft.csrfTokenValue; + } + + return new Promise((resolve, reject) => { + Craft.sendActionRequest('POST', $form.data('action'), { + data: params + }) + .then((response) => { + window.location.reload(); + + let element = response.data.element; + + if (Craft.broadcaster) { + Craft.broadcaster.postMessage({ + pageId: Craft.pageId, + event: 'saveDraft', + canonicalId: element.canonicalId, + draftId: element.draftId, + isProvisionalDraft: element.isProvisionalDraft, + }); + } + + resolve(); + }) + .catch((e) => { + this.setStatusMessage(e.message); + let $errorContainer = $form.find('p.error'); + if ($form.find('p.error').length > 0) { + $errorContainer.contents(e.response.data.message); + } else { + $form.append('

' + e.response.data.message + '

'); + } + reject(e); + }); + }); + }, + /** * @returns {Array} */ From e51cac08a8018b8cba80baa62277f85dd7a61a4c Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Thu, 1 Dec 2022 14:01:43 +0000 Subject: [PATCH 02/26] show notifications --- src/controllers/ElementsController.php | 40 +++++++++++++---------- src/services/Elements.php | 14 +++++--- src/translations/en/app.php | 5 +++ src/web/assets/cp/src/js/ElementEditor.js | 3 ++ 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index cce0d1bb10a..4f8831c8e2a 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -575,7 +575,7 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null): return $response; } - public function actionCopyFieldValueFromSite() + public function actionCopyFieldValueFromSite(): Response { $this->requireAcceptsJson(); @@ -586,28 +586,31 @@ public function actionCopyFieldValueFromSite() throw new BadRequestHttpException('No element was identified by the request.'); } - $params = $this->request->getBodyParams(); - $fieldHandle = $params['fieldHandle'] ?? null; - $copyFromSiteId = $params['copyFromSiteId'] ?? null; - - if ($fieldHandle === null || $copyFromSiteId === null) { - throw new BadRequestHttpException('No field handle or site id to copy from provided.'); - } + $fieldHandle = $this->request->getRequiredBodyParam('fieldHandle'); + $copyFromSiteId = $this->request->getRequiredBodyParam('copyFromSiteId'); $elementsService = Craft::$app->getElements(); $user = static::currentUser(); // check if this entry exists for other sites if (empty($siteIdsForElement = $elementsService->getEnabledSiteIdsForElement($element->id))) { - return $this->asFailure(Craft::t('app', 'Couldn\'t find this {type} in other sites.', [ + $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)) { - return $this->asFailure(Craft::t('app', 'Couldn\'t find this {type} for the site you selected.', [ + $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); } // todo: IWONA check if we need those checks! @@ -619,8 +622,11 @@ public function actionCopyFieldValueFromSite() throw new ForbiddenHttpException('User not authorized to save this element.'); } - if (!$elementsService->copyFieldValueFromSite($element, $fieldHandle, $copyFromSiteId)) { - $this->_asFailure($element, Craft::t('app', 'Couldn\'t copy the value.')); + $result = $elementsService->copyFieldValueFromSite($element, $fieldHandle, $copyFromSiteId); + if ($result['success'] === false) { + Craft::$app->session->setError($result['message']); + + return $this->_asFailure($result['element'], $result['message']); } if (!$this->request->getAcceptsJson()) { @@ -631,9 +637,9 @@ public function actionCopyFieldValueFromSite() ]); } - return $this->_asSuccess(Craft::t('app', 'Value copied.', [ - 'type' => $element::displayName(), - ]), $element); + Craft::$app->session->setNotice($result['message']); + + return $this->_asSuccess($result['message'], $result['element']); } /** diff --git a/src/services/Elements.php b/src/services/Elements.php index 4d16f1e03bd..09e3e98b491 100644 --- a/src/services/Elements.php +++ b/src/services/Elements.php @@ -2156,9 +2156,10 @@ public function copyFieldValueFromSite(ElementInterface $element, string $fieldH if (!$elementForSite) { return [ 'success' => false, - 'message' => Craft::t('app', 'Couldn\'t find this {type} for the site you selected.', [ + 'message' => Craft::t('app', 'Couldn’t find this {type} on the site you selected.', [ 'type' => $element::lowerDisplayName(), - ]) + ]), + 'element' => $element, ]; } @@ -2195,14 +2196,16 @@ public function copyFieldValueFromSite(ElementInterface $element, string $fieldH if (!$this->saveElement($element, true, false, false)) { return [ 'success' => false, - 'message' => Craft::t('app', 'Couldn\'t copy the value.') + 'message' => Craft::t('app', 'Couldn’t copy the value.'), + 'element' => $element, ]; } $transaction->commit(); return [ 'success' => true, - 'message' => Craft::t('app', 'Value copied.') + 'message' => Craft::t('app', 'Value copied.'), + 'element' => $element, ]; } catch (Throwable $e) { $transaction->rollBack(); @@ -2213,7 +2216,8 @@ public function copyFieldValueFromSite(ElementInterface $element, string $fieldH return [ 'success' => true, - 'message' => Craft::t('app', 'Nothing to copy.') + 'message' => Craft::t('app', 'Nothing to copy.'), + 'element' => $element, ]; } diff --git a/src/translations/en/app.php b/src/translations/en/app.php index b46bcfe976c..175c99def80 100644 --- a/src/translations/en/app.php +++ b/src/translations/en/app.php @@ -341,6 +341,7 @@ 'Couldn’t apply draft.' => 'Couldn’t apply draft.', 'Couldn’t apply new migrations.' => 'Couldn’t apply new migrations.', 'Couldn’t backup the database. How would you like to proceed?' => 'Couldn’t backup the database. How would you like to proceed?', + 'Couldn’t copy the value.' => 'Couldn’t copy the value.', 'Couldn’t create {type}.' => 'Couldn’t create {type}.', 'Couldn’t delete asset.' => 'Couldn’t delete asset.', 'Couldn’t delete the user.' => 'Couldn’t delete the user.', @@ -351,6 +352,8 @@ 'Couldn’t duplicate entry.' => 'Couldn’t duplicate entry.', 'Couldn’t duplicate {type}.' => 'Couldn’t duplicate {type}.', 'Couldn’t enable plugin.' => 'Couldn’t enable plugin.', + 'Couldn’t find this {type} on other sites.' => 'Couldn’t find this {type} on other sites.', + 'Couldn’t find this {type} on the site you selected.' => 'Couldn’t find this {type} on the site you selected.', 'Couldn’t install plugin.' => 'Couldn’t install plugin.', 'Couldn’t load CMS editions.' => 'Couldn’t load CMS editions.', 'Couldn’t load active trials.' => 'Couldn’t load active trials.', @@ -1002,6 +1005,7 @@ 'Notes about your changes' => 'Notes about your changes', 'Notes' => 'Notes', 'Nothing selected.' => 'Nothing selected.', + 'Nothing to copy.' => 'Nothing to copy.', 'Nothing to index.' => 'Nothing to index.', 'Nothing to update.' => 'Nothing to update.', 'Notice' => 'Notice', @@ -1672,6 +1676,7 @@ 'Value prefixed by “{prefix}”.' => 'Value prefixed by “{prefix}”.', 'Value suffixed by “{suffix}”.' => 'Value suffixed by “{suffix}”.', 'Value' => 'Value', + 'Value copied.' => 'Value copied.', 'Verify email addresses' => 'Verify email addresses', 'Version {version}' => 'Version {version}', 'Version' => 'Version', diff --git a/src/web/assets/cp/src/js/ElementEditor.js b/src/web/assets/cp/src/js/ElementEditor.js index 648a999e42c..0d0170deb61 100644 --- a/src/web/assets/cp/src/js/ElementEditor.js +++ b/src/web/assets/cp/src/js/ElementEditor.js @@ -614,6 +614,9 @@ Craft.ElementEditor = Garnish.Base.extend( $errorContainer.contents(e.response.data.message); } else { $form.append('

' + e.response.data.message + '

'); + }*/ + if (e.response.data.message) { + Craft.cp.displayError(e.response.data.message); } reject(e); }); From 7dc40cf943025cf53d99d60bc7772dc9047c61f2 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Thu, 1 Dec 2022 14:01:55 +0000 Subject: [PATCH 03/26] copy only top level fields --- src/web/assets/cp/src/js/ElementEditor.js | 41 +++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/web/assets/cp/src/js/ElementEditor.js b/src/web/assets/cp/src/js/ElementEditor.js index 0d0170deb61..50e74a26572 100644 --- a/src/web/assets/cp/src/js/ElementEditor.js +++ b/src/web/assets/cp/src/js/ElementEditor.js @@ -535,24 +535,29 @@ Craft.ElementEditor = Garnish.Base.extend( $btn = $(ev.target); - let form = - '
' + - Craft.getCsrfInput() + - '' + - '' + - '' + - '' + - '
'; - - hud = new Garnish.HUD( - $btn, - `
` + + const immediateFieldParentId = $btn.parents('.field:first').attr('id'); + const topFieldParentId = $btn.parents('.field:last').attr('id'); + + let hudContent = `
` + `` + $btn.attr('title') + - `
` + - form + - `
` - ); + ``; + + // only allow the copy field value on the top-level field (e.g. entire matrix field and not it's blocks) + if (immediateFieldParentId == topFieldParentId) { + hudContent += `
` + + '
' + + Craft.getCsrfInput() + + '' + + '' + + '' + + '' + + '
'; + } + + hudContent += `
`; + + hud = new Garnish.HUD($btn, hudContent); this.addListener($('.copyTranslationForField'), 'submit', 'copyTranslatedValueFromSite'); @@ -608,8 +613,8 @@ Craft.ElementEditor = Garnish.Base.extend( resolve(); }) .catch((e) => { - this.setStatusMessage(e.message); - let $errorContainer = $form.find('p.error'); + this.setStatusMessage(e.response.data.message); + /*let $errorContainer = $form.find('p.error'); if ($form.find('p.error').length > 0) { $errorContainer.contents(e.response.data.message); } else { From 3f2ce0c95fc58f0d7c04c55da6a54a124b2879e9 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Thu, 1 Dec 2022 15:57:50 +0000 Subject: [PATCH 04/26] site select hooked up --- src/controllers/ElementsController.php | 4 +++ src/web/assets/cp/src/js/ElementEditor.js | 39 ++++++++++++++--------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 4f8831c8e2a..a622b76efb0 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -589,6 +589,10 @@ public function actionCopyFieldValueFromSite(): Response $fieldHandle = $this->request->getRequiredBodyParam('fieldHandle'); $copyFromSiteId = $this->request->getRequiredBodyParam('copyFromSiteId'); + if (empty($fieldHandle) || empty($copyFromSiteId)) { + throw new BadRequestHttpException("Request missing required param"); + } + $elementsService = Craft::$app->getElements(); $user = static::currentUser(); diff --git a/src/web/assets/cp/src/js/ElementEditor.js b/src/web/assets/cp/src/js/ElementEditor.js index 50e74a26572..e6825279521 100644 --- a/src/web/assets/cp/src/js/ElementEditor.js +++ b/src/web/assets/cp/src/js/ElementEditor.js @@ -52,6 +52,8 @@ Craft.ElementEditor = Garnish.Base.extend( previewLinks: null, scrollY: null, + sitesForCopyFieldAction: null, + get slideout() { return this.$container.data('slideout'); }, @@ -124,6 +126,8 @@ Craft.ElementEditor = Garnish.Base.extend( 'expandSiteStatuses' ); + this.sitesForCopyFieldAction = this._getSitesForCopyFieldAction(); + this.addListener(this.copyTranslationBtns, 'click', 'showFieldTranslationDialogue'); } @@ -530,6 +534,18 @@ Craft.ElementEditor = Garnish.Base.extend( this._updateGlobalStatus(); }, + _getSitesForCopyFieldAction: function() { + var menuOptions = []; + this._getOtherSupportedSites().forEach((s) => + menuOptions.push({ + label: s.name, + value: s.id + }) + ); + + return menuOptions; + }, + showFieldTranslationDialogue: function (ev) { ev.preventDefault(); @@ -544,13 +560,19 @@ Craft.ElementEditor = Garnish.Base.extend( ``; // only allow the copy field value on the top-level field (e.g. entire matrix field and not it's blocks) - if (immediateFieldParentId == topFieldParentId) { + if (immediateFieldParentId == topFieldParentId && this.sitesForCopyFieldAction.length > 0) { + let select = '
'; + hudContent += `
` + '
' + Craft.getCsrfInput() + '' + '' + - '' + + select + '' + '
'; } @@ -560,19 +582,6 @@ Craft.ElementEditor = Garnish.Base.extend( hud = new Garnish.HUD($btn, hudContent); this.addListener($('.copyTranslationForField'), 'submit', 'copyTranslatedValueFromSite'); - - /*var menuOptions = [{ - label: this.settings.siteId, - value: this.settings.siteId - }]; - this._getOtherSupportedSites().forEach((s) => - menuOptions.push({ - label: s.name, - value: s.id - }) - ); - console.log(menuOptions); - new Garnish.ContextMenu($("#copyTranslationSiteId"), menuOptions, {menuClass: 'menu'});*/ }, copyTranslatedValueFromSite: function (ev) { From bfe9ff4fe176f92ab922be113d9615bcd6eee621 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Fri, 2 Dec 2022 10:23:39 +0000 Subject: [PATCH 05/26] additional safeguards; adjust for title, slug; bug fixes --- src/controllers/ElementsController.php | 29 ++++--- src/services/Elements.php | 97 +++++++++++++---------- src/translations/en/app.php | 1 + src/web/assets/cp/CpAsset.php | 1 + src/web/assets/cp/dist/cp.js | 2 +- src/web/assets/cp/dist/cp.js.map | 2 +- src/web/assets/cp/dist/css/cp.css | 2 +- src/web/assets/cp/dist/css/cp.css.map | 2 +- src/web/assets/cp/src/css/_main.scss | 6 ++ src/web/assets/cp/src/js/ElementEditor.js | 65 ++++++++++----- 10 files changed, 133 insertions(+), 74 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index a622b76efb0..7950092d669 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -575,6 +575,20 @@ 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 actionCopyFieldValueFromSite(): Response { $this->requireAcceptsJson(); @@ -596,6 +610,12 @@ public function actionCopyFieldValueFromSite(): Response $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.', [ @@ -617,15 +637,6 @@ public function actionCopyFieldValueFromSite(): Response return $this->_asFailure($element, $errorMsg); } - // todo: IWONA check if we need those checks! - if (!$element->getIsDraft() && !$this->_provisional) { - if (!$elementsService->canCreateDrafts($element, $user)) { - throw new ForbiddenHttpException('User not authorized to create drafts for this element.'); - } - } elseif (!$this->_canSave($element, $user)) { - throw new ForbiddenHttpException('User not authorized to save this element.'); - } - $result = $elementsService->copyFieldValueFromSite($element, $fieldHandle, $copyFromSiteId); if ($result['success'] === false) { Craft::$app->session->setError($result['message']); diff --git a/src/services/Elements.php b/src/services/Elements.php index 09e3e98b491..4121c95b424 100644 --- a/src/services/Elements.php +++ b/src/services/Elements.php @@ -2143,15 +2143,31 @@ public function restoreElements(array $elements): bool return true; } + /** + * Copy value of a field from another site + * + * @param ElementInterface $element + * @param string $fieldHandle + * @param int $copyFromSiteId + * @return array + * @throws ElementNotFoundException + * @throws Exception + * @throws Throwable + * @throws \craft\errors\InvalidFieldException + * @throws \yii\db\Exception + */ public function copyFieldValueFromSite(ElementInterface $element, string $fieldHandle, int $copyFromSiteId): array { + // reserved $fieldHandles which we need to treat differently + $reservedHandles = ['title', 'slug']; + // get element for selected site /** @var string|ElementInterface $elementType */ $elementType = get_class($element); $user = Craft::$app->getUser()->getIdentity(); - $elementForSite = $this->getElementById($element->canonicalId, $elementType, $copyFromSiteId); + $elementForSite = $this->getElementById($element->getCanonicalId(), $elementType, $copyFromSiteId); if (!$elementForSite) { return [ @@ -2163,54 +2179,53 @@ public function copyFieldValueFromSite(ElementInterface $element, string $fieldH ]; } - $currentValue = $element->getFieldValue($fieldHandle); - $copyValue = $elementForSite->getFieldValue($fieldHandle); - - if (($field = Craft::$app->fields->getFieldByHandle($fieldHandle)) !== null) { - $currentValue = $field->serializeValue($currentValue); - $copyValue = $field->serializeValue($copyValue); + if (in_array($fieldHandle, $reservedHandles)) { + $currentValue = $element->{$fieldHandle}; + $copiedValue = $elementForSite->{$fieldHandle}; + } else { + $currentValue = $element->getFieldValue($fieldHandle); + $copiedValue = $elementForSite->getFieldValue($fieldHandle); - if ($currentValue != $copyValue) { - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - /*if ($value instanceof MatrixBlockQuery) { - $field = Craft::$app->fields->getFieldByHandle($fieldHandle); - $test = $field->serializeValue($value); - $elementClone = clone($element); - $elementClone->canonicalId = null; - Craft::$app->matrix->duplicateBlocks($field, $elementForSite, $elementClone, trackDuplications: false); - } else { - $element->setFieldValue($fieldHandle, $value); - }*/ - - // check if element is a draft - if not, create one - if (!$element->getIsDraft()) { - /** @var Element|DraftBehavior $element */ - $draft = Craft::$app->getDrafts()->createDraft($element, $user->id, null, null, [], true); - $draft->setCanonical($element); - $element = $draft; - } + if (($field = Craft::$app->fields->getFieldByHandle($fieldHandle)) !== null) { + $currentValue = $field->serializeValue($currentValue); + $copiedValue = $field->serializeValue($copiedValue); + } + } - $element->setFieldValue($fieldHandle, $copyValue); + if ($currentValue != $copiedValue) { + $transaction = Craft::$app->getDb()->beginTransaction(); + try { + // check if element is a draft - if not, create one + if (!$element->getIsDraft()) { + /** @var Element|DraftBehavior $element */ + $draft = Craft::$app->getDrafts()->createDraft($element, $user->id, null, null, [], true); + $draft->setCanonical($element); + $element = $draft; + } - if (!$this->saveElement($element, true, false, false)) { - return [ - 'success' => false, - 'message' => Craft::t('app', 'Couldn’t copy the value.'), - 'element' => $element, - ]; - } + if (in_array($fieldHandle, $reservedHandles)) { + $element->{$fieldHandle} = $copiedValue; + } else { + $element->setFieldValue($fieldHandle, $copiedValue); + } - $transaction->commit(); + if (!$this->saveElement($element, true, false, false)) { return [ - 'success' => true, - 'message' => Craft::t('app', 'Value copied.'), + 'success' => false, + 'message' => Craft::t('app', 'Couldn’t copy the value.'), 'element' => $element, ]; - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; } + + $transaction->commit(); + return [ + 'success' => true, + 'message' => Craft::t('app', 'Value copied.'), + 'element' => $element, + ]; + } catch (Throwable $e) { + $transaction->rollBack(); + throw $e; } } diff --git a/src/translations/en/app.php b/src/translations/en/app.php index 175c99def80..b02e121e296 100644 --- a/src/translations/en/app.php +++ b/src/translations/en/app.php @@ -316,6 +316,7 @@ 'Copied to clipboard.' => 'Copied to clipboard.', 'Copy URL' => 'Copy URL', 'Copy activation URL…' => 'Copy activation URL…', + 'Copy field value from:' => 'Copy field value from:', 'Copy impersonation URL…' => 'Copy impersonation URL…', 'Copy password reset URL…' => 'Copy password reset URL…', 'Copy reference tag' => 'Copy reference tag', diff --git a/src/web/assets/cp/CpAsset.php b/src/web/assets/cp/CpAsset.php index 9bf5c21bfe5..e4b80473bfa 100644 --- a/src/web/assets/cp/CpAsset.php +++ b/src/web/assets/cp/CpAsset.php @@ -143,6 +143,7 @@ private function _registerTranslations(View $view): void 'Color picker', 'Continue', 'Copied to clipboard.', + 'Copy field value from:', 'Copy the URL', 'Copy the reference tag', 'Copy to clipboard', diff --git a/src/web/assets/cp/dist/cp.js b/src/web/assets/cp/dist/cp.js index 4b04b7d4bb3..d043450f8f4 100644 --- a/src/web/assets/cp/dist/cp.js +++ b/src/web/assets/cp/dist/cp.js @@ -1,2 +1,2 @@ -(function(){var __webpack_modules__={3839:function(){Craft.AddressesInput=Garnish.Base.extend({$container:null,$addBtn:null,$addBtnItem:null,$cards:null,init:function(t,e){var i=this;this.$container=$(t),this.setSettings(e,Craft.AddressesInput.defaults),this.$container.data("addresses")&&(console.warn("Double-instantiating an address input on an element"),this.$container.data("addresses").destroy()),this.$container.data("addresses",this),this.$addBtn=this.$container.find(".address-cards__add-btn"),this.$addBtnItem=this.$addBtn.closest("li"),this.$cards=this.$container.find("> .address-card");for(var s=0;s=this.settings.maxItems)){var e=$(t).appendTo(this.$tbody),i=e.find(".delete");this.settings.sortable&&this.sorter.addItems(e),this.$deleteBtns=this.$deleteBtns.add(i),this.addListener(i,"click","handleDeleteBtnClick"),this.totalItems++,this.updateUI()}},reorderItems:function(){var t=this;if(this.settings.sortable){for(var e=[],i=0;i=this.settings.maxItems?$(this.settings.newItemBtnSelector).addClass("hidden"):$(this.settings.newItemBtnSelector).removeClass("hidden"))}},{defaults:{tableSelector:null,noItemsSelector:null,newItemBtnSelector:null,idAttribute:"data-id",nameAttribute:"data-name",sortable:!1,allowDeleteAll:!0,minItems:0,maxItems:null,reorderAction:null,deleteAction:null,reorderSuccessMessage:Craft.t("app","New order saved."),reorderFailMessage:Craft.t("app","Couldn’t save new order."),confirmDeleteMessage:Craft.t("app","Are you sure you want to delete “{name}”?"),deleteSuccessMessage:Craft.t("app","“{name}” deleted."),deleteFailMessage:Craft.t("app","Couldn’t delete “{name}”."),onReorderItems:$.noop,onDeleteItem:$.noop}})},6872:function(){Craft.AssetImageEditor=Garnish.Modal.extend({$body:null,$footer:null,$imageTools:null,$buttons:null,$cancelBtn:null,$replaceBtn:null,$saveBtn:null,$focalPointBtn:null,$editorContainer:null,$straighten:null,$croppingCanvas:null,$spinner:null,$constraintContainer:null,$constraintRadioInputs:null,$customConstraints:null,canvas:null,image:null,viewport:null,focalPoint:null,grid:null,croppingCanvas:null,clipper:null,croppingRectangle:null,cropperHandles:null,cropperGrid:null,croppingShade:null,imageStraightenAngle:0,viewportRotation:0,originalWidth:0,originalHeight:0,imageVerticeCoords:null,zoomRatio:1,animationInProgress:!1,currentView:"",assetId:null,cacheBust:null,draggingCropper:!1,scalingCropper:!1,draggingFocal:!1,previousMouseX:0,previousMouseY:0,shiftKeyHeld:!1,editorHeight:0,editorWidth:0,cropperState:!1,scaleFactor:1,flipData:{},focalPointState:!1,maxImageSize:null,lastLoadedDimensions:null,imageIsLoading:!1,mouseMoveEvent:null,croppingConstraint:!1,constraintOrientation:"landscape",showingCustomConstraint:!1,saving:!1,renderImage:null,renderCropper:null,_queue:null,init:function(t,e){var i=this;this._queue=new Craft.Queue,this.cacheBust=Date.now(),this.setSettings(e,Craft.AssetImageEditor.defaults),null===this.settings.allowDegreeFractions&&(this.settings.allowDegreeFractions=Craft.isImagick),Garnish.prefersReducedMotion()&&(this.settings.animationDuration=1),this.assetId=t,this.flipData={x:0,y:0},this.$container=$('').appendTo(Garnish.$bod),this.$body=$('
').appendTo(this.$container),this.$footer=$('