diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b8d97ed..aeaf723a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Changed - reflect changes that was made in https://github.com/craftcms/cms/commit/80192a55f8f89b129abff2b43d4a0c7d66d60f45 +### Fixed +- Fix issue #270 + ## 2.5.3 ### Changed - Stop the blocktype always recreating a fieldlayout.uid - thanks @samuelbirch diff --git a/src/Field.php b/src/Field.php index a7a77445..3abcf8a5 100644 --- a/src/Field.php +++ b/src/Field.php @@ -1,4 +1,5 @@ localizeBlocks = $this->propagationMethod === self::PROPAGATION_METHOD_NONE; - parent::init(); - } - - /** - * @inheritdoc - */ - public function settingsAttributes(): array - { - return ArrayHelper::withoutValue(parent::settingsAttributes(), 'localizeBlocks'); - } - + + /** + * @var string Propagation method + * + * This will be set to one of the following: + * + * - `none` – Only save b locks in the site they were created in + * - `siteGroup` – Save blocks to other sites in the same site group + * - `language` – Save blocks to other sites with the same language + * - `all` – Save blocks to all sites supported by the owner element + * + * @since 2.4.0 + */ + public $propagationMethod = self::PROPAGATION_METHOD_ALL; + + /** + * @var string The old propagation method for this field + */ + private $_oldPropagationMethod; + + // Public Methods + // ========================================================================= + /** + * @inheritdoc + */ + public function __construct($config = []) + { + if (array_key_exists('localizeBlocks', $config)) { + $config['propagationMethod'] = $config['localizeBlocks'] ? 'none' : 'all'; + unset($config['localizeBlocks']); + } + parent::__construct($config); + } + + /** + * @inheritdoc + */ + public function init() + { + // Set localizeBlocks in case anything is still checking it + $this->localizeBlocks = $this->propagationMethod === self::PROPAGATION_METHOD_NONE; + parent::init(); + } + + /** + * @inheritdoc + */ + public function settingsAttributes(): array + { + return ArrayHelper::withoutValue(parent::settingsAttributes(), 'localizeBlocks'); + } + /** * @inheritdoc */ public function rules(): array { $rules = parent::rules(); - $rules[] = [ - ['propagationMethod'], 'in', 'range' => [ - self::PROPAGATION_METHOD_NONE, - self::PROPAGATION_METHOD_SITE_GROUP, - self::PROPAGATION_METHOD_LANGUAGE, - self::PROPAGATION_METHOD_ALL - ] - ]; + $rules[] = [ + ['propagationMethod'], + 'in', + 'range' => [ + self::PROPAGATION_METHOD_NONE, + self::PROPAGATION_METHOD_SITE_GROUP, + self::PROPAGATION_METHOD_LANGUAGE, + self::PROPAGATION_METHOD_ALL + ] + ]; $rules[] = [['minBlocks', 'maxBlocks', 'maxTopBlocks'], 'integer', 'min' => 0]; - + return $rules; } - + /** * Returns this field's block types. * @@ -183,23 +187,19 @@ public function rules(): array public function getBlockTypes(): array { $blockTypes = $this->_blockTypes; - - if ($blockTypes === null) - { - if ($this->getIsNew()) - { + + if ($blockTypes === null) { + if ($this->getIsNew()) { $blockTypes = []; - } - else - { + } else { $blockTypes = Neo::$plugin->blockTypes->getByFieldId($this->id); $this->_blockTypes = $blockTypes; } } - + return $blockTypes; } - + /** * Sets this field's block types. * @@ -209,12 +209,10 @@ public function setBlockTypes($blockTypes) { $newBlockTypes = []; - foreach ($blockTypes as $blockTypeId => $blockType) - { + foreach ($blockTypes as $blockTypeId => $blockType) { $newBlockType = $blockType; - - if (!($blockType instanceof BlockType)) - { + + if (!($blockType instanceof BlockType)) { $newBlockType = new BlockType(); $newBlockType->id = $blockTypeId; $newBlockType->fieldId = $this->id; @@ -225,46 +223,42 @@ public function setBlockTypes($blockTypes) $newBlockType->topLevel = (bool)$blockType['topLevel']; $newBlockType->childBlocks = $blockType['childBlocks']; $newBlockType->sortOrder = (int)$blockType['sortOrder']; - - if (!empty($blockType['fieldLayout'])) - { + + if (!empty($blockType['fieldLayout'])) { $fieldLayoutPost = $blockType['fieldLayout']; $requiredFieldPost = empty($blockType['requiredFields']) ? [] : $blockType['requiredFields']; - + // Add support for blank tabs - foreach ($fieldLayoutPost as $tabName => $fieldIds) - { + foreach ($fieldLayoutPost as $tabName => $fieldIds) { $fieldLayoutPost[$tabName] = is_array($fieldIds) ? $fieldIds : []; } - + $fieldLayout = Craft::$app->getFields()->assembleLayout($fieldLayoutPost, $requiredFieldPost); $fieldLayout->type = Block::class; - + // Ensure the field layout ID is set, if it exists - if (is_int($blockTypeId)) - { + if (is_int($blockTypeId)) { $layoutIdResult = (new Query) ->select(['fieldLayoutId']) ->from('{{%neoblocktypes}}') ->where(['id' => $blockTypeId]) ->one(); - - if ($layoutIdResult !== null) - { + + if ($layoutIdResult !== null) { $fieldLayout->id = $layoutIdResult['fieldLayoutId']; } } - + $newBlockType->setFieldLayout($fieldLayout); } } $newBlockTypes[] = $newBlockType; } - + $this->_blockTypes = $newBlockTypes; } - + /** * Returns this field's block type groups. * @@ -273,23 +267,19 @@ public function setBlockTypes($blockTypes) public function getGroups(): array { $blockTypeGroups = $this->_blockTypeGroups; - - if ($blockTypeGroups === null) - { - if ($this->getIsNew()) - { + + if ($blockTypeGroups === null) { + if ($this->getIsNew()) { $blockTypeGroups = []; - } - else - { + } else { $blockTypeGroups = Neo::$plugin->blockTypes->getGroupsByFieldId($this->id); $this->_blockTypeGroups = $blockTypeGroups; } } - + return $blockTypeGroups; } - + /** * Sets this field's block type groups. * @@ -299,12 +289,10 @@ public function setGroups($blockTypeGroups) { $newBlockTypeGroups = []; - foreach ($blockTypeGroups as $blockTypeGroup) - { + foreach ($blockTypeGroups as $blockTypeGroup) { $newBlockTypeGroup = $blockTypeGroup; - - if (!($blockTypeGroup instanceof BlockTypeGroup)) - { + + if (!($blockTypeGroup instanceof BlockTypeGroup)) { $newBlockTypeGroup = new BlockTypeGroup(); $newBlockTypeGroup->fieldId = $this->id; $newBlockTypeGroup->name = $blockTypeGroup['name']; @@ -313,10 +301,10 @@ public function setGroups($blockTypeGroups) $newBlockTypeGroups[] = $newBlockTypeGroup; } - + $this->_blockTypeGroups = $newBlockTypeGroups; } - + /** * @inheritdoc */ @@ -324,35 +312,32 @@ public function validate($attributeNames = null, $clearErrors = true): bool { $validates = parent::validate($attributeNames, $clearErrors); $validates = $validates && Neo::$plugin->fields->validate($this); - + return $validates; } - + /** * @inheritdoc */ public function getSettingsHtml() { $viewService = Craft::$app->getView(); - + $html = ''; - + // Disable creating Neo fields inside Matrix, SuperTable and potentially other field-grouping field types. - if ($this->_getNamespaceDepth() >= 1) - { + if ($this->_getNamespaceDepth() >= 1) { $html = $this->_getNestingErrorHtml(); - } - else - { + } else { $viewService->registerAssetBundle(FieldAsset::class); $viewService->registerJs(FieldAsset::createSettingsJs($this)); - + $html = $viewService->renderTemplate('neo/settings', ['neoField' => $this]); } - + return $html; } - + /** * @inheritdoc */ @@ -360,7 +345,7 @@ public function getInputHtml($value, ElementInterface $element = null): string { return $this->_getInputHtml($value, $element); } - + /** * @inheritdoc */ @@ -368,76 +353,64 @@ public function getStaticHtml($value, ElementInterface $element): string { return $this->_getInputHtml($value, $element, true); } - + /** * @inheritdoc */ public function normalizeValue($value, ElementInterface $element = null) { $query = null; - - if ($value instanceof ElementQueryInterface) - { + + if ($value instanceof ElementQueryInterface) { return $value; } - - $query = Block::find(); - $blockStructure = null; - - // Existing element? - $existingElement = $element && $element->id; - if ($existingElement) - { - $query->ownerId($element->id); - } - else - { - $query->id(false); - } - - $query->fieldId($this->id)->siteId($element->siteId ?? null); - - // If an owner element exists, set the appropriate owner site ID and block structure, depending on whether - // the field is set to manage blocks on a per-site basis - if ($existingElement) - { - $blockStructure = Neo::$plugin->blocks->getStructure($this->id, $element->id, (int)$element->siteId); - } - - // If we found the block structure, set the query's structure ID - if ($blockStructure) - { - $query->structureId($blockStructure->structureId); - } - - // Set the initially matched elements if $value is already set, which is the case if there was a validation - // error or we're loading an entry revision. - if (is_array($value) || $value === '') - { - $elements = $this->_createBlocksFromSerializedData($value, $element); - - if (!Craft::$app->getRequest()->getIsLivePreview()) - { - $query->anyStatus(); - } - else - { - $query->status = Element::STATUS_ENABLED; - } - - $query->limit = null; - // don't set the cached result if element is a draft initially. + + $query = Block::find(); + $blockStructure = null; + + // Existing element? + $existingElement = $element && $element->id; + if ($existingElement) { + $query->ownerId($element->id); + } else { + $query->id(false); + } + + $query->fieldId($this->id)->siteId($element->siteId ?? null); + + // If an owner element exists, set the appropriate owner site ID and block structure, depending on whether + // the field is set to manage blocks on a per-site basis + if ($existingElement) { + $blockStructure = Neo::$plugin->blocks->getStructure($this->id, $element->id, (int)$element->siteId); + } + + // If we found the block structure, set the query's structure ID + if ($blockStructure) { + $query->structureId($blockStructure->structureId); + } + + // Set the initially matched elements if $value is already set, which is the case if there was a validation + // error or we're loading an entry revision. + if (is_array($value) || $value === '') { + $elements = $this->_createBlocksFromSerializedData($value, $element); + + if (!Craft::$app->getRequest()->getIsLivePreview()) { + $query->anyStatus(); + } else { + $query->status = Element::STATUS_ENABLED; + } + + $query->limit = null; + // don't set the cached result if element is a draft initially. // on draft creation the all other sites (in a multisite) uses the cached result from the element (where the draft came from) // which overwrites the contents on the other sites. - if (!$element->getIsDraft() || ($element->getIsDraft() && count($element->newSiteIds) === 0)) { - $query->setCachedResult($elements); - $query->useMemoized($elements); - } - } - + $query->setCachedResult($elements); + $query->useMemoized($elements); + } + return $query; } - + /** * @inheritdoc */ @@ -445,9 +418,8 @@ public function serializeValue($value, ElementInterface $element = null) { $serialized = []; $new = 0; - - foreach ($value->all() as $block) - { + + foreach ($value->all() as $block) { $blockId = $block->id ?? 'new' . ++$new; $serialized[$blockId] = [ 'type' => $block->getType()->handle, @@ -457,38 +429,34 @@ public function serializeValue($value, ElementInterface $element = null) 'fields' => $block->getSerializedFieldValues(), ]; } - + return $serialized; } - + /** * @inheritdoc */ public function modifyElementsQuery(ElementQueryInterface $query, $value) { - if ($value === 'not :empty:') - { + if ($value === 'not :empty:') { $value = ':notempty:'; } - - if ($value === ':notempty:' || $value === ':empty:') - { + + if ($value === ':notempty:' || $value === ':empty:') { $alias = 'neoblocks_' . $this->handle; $operator = $value === ':notempty:' ? '!=' : '='; - + $query->subQuery->andWhere( "(select count([[{$alias}.id]]) from {{%neoblocks}} {{{$alias}}} where [[{$alias}.ownerId]] = [[elements.id]] and [[{$alias}.fieldId]] = :fieldId) {$operator} 0", [':fieldId' => $this->id] ); - } - elseif ($value !== null) - { + } elseif ($value !== null) { return false; } - + return null; } - + /** * @inheritdoc */ @@ -496,7 +464,7 @@ public function getIsTranslatable(ElementInterface $element = null): bool { return $this->propagationMethod !== self::PROPAGATION_METHOD_ALL; } - + /** * @inheritdoc */ @@ -508,8 +476,10 @@ public function getElementValidationRules(): array ArrayValidator::class, 'min' => $this->minBlocks ?: null, 'max' => $this->maxBlocks ?: null, - 'tooFew' => Craft::t('neo', '{attribute} should contain at least {min, number} {min, plural, one{block} other{blocks}}.'), - 'tooMany' => Craft::t('neo', '{attribute} should contain at most {max, number} {max, plural, one{block} other{blocks}}.'), + 'tooFew' => Craft::t('neo', + '{attribute} should contain at least {min, number} {min, plural, one{block} other{blocks}}.'), + 'tooMany' => Craft::t('neo', + '{attribute} should contain at most {max, number} {max, plural, one{block} other{blocks}}.'), 'skipOnEmpty' => false, 'on' => Element::SCENARIO_LIVE, ], @@ -520,7 +490,7 @@ public function getElementValidationRules(): array ], ]; } - + /** * @inheritdoc */ @@ -528,7 +498,7 @@ public function isValueEmpty($value, ElementInterface $element): bool { return $value->count() === 0; } - + /** * Perform validation on blocks belonging to this field for a given element. * @@ -537,48 +507,43 @@ public function isValueEmpty($value, ElementInterface $element): bool public function validateBlocks(ElementInterface $element) { $value = $element->getFieldValue($this->handle); - - foreach ($value->all() as $key => $block) - { - if ($element->getScenario() === Element::SCENARIO_LIVE) - { + + foreach ($value->all() as $key => $block) { + if ($element->getScenario() === Element::SCENARIO_LIVE) { $block->setScenario(Element::SCENARIO_LIVE); } - - if (!$block->validate()) - { + + if (!$block->validate()) { $element->addModelErrors($block, "{$this->handle}[{$key}]"); } } } - + /** * @inheritdoc */ public function getSearchKeywords($value, ElementInterface $element): string { $keywords = []; - - foreach ($value->all() as $block) - { + + foreach ($value->all() as $block) { $keywords[] = Neo::$plugin->blocks->getSearchKeywords($block); } - + return parent::getSearchKeywords($keywords, $element); } - + /** * @inheritdoc */ public function getEagerLoadingMap(array $sourceElements) { $sourceElementIds = []; - - foreach ($sourceElements as $sourceElement) - { + + foreach ($sourceElements as $sourceElement) { $sourceElementIds[] = $sourceElement->id; } - + // Return any relation data on these elements, defined with this field. $map = (new Query()) ->select(['[[neoblocks.ownerId]] as source', '[[neoblocks.id]] as target']) @@ -594,7 +559,7 @@ public function getEagerLoadingMap(array $sourceElements) 'and', '[[neoblockstructures.ownerId]] = [[neoblocks.ownerId]]', '[[neoblockstructures.fieldId]] = [[neoblocks.fieldId]]', - '[[neoblockstructures.ownerSiteId]] = '. Craft::$app->getSites()->getCurrentSite()->id, + '[[neoblockstructures.ownerSiteId]] = ' . Craft::$app->getSites()->getCurrentSite()->id, ] ) ->leftJoin( @@ -607,7 +572,7 @@ public function getEagerLoadingMap(array $sourceElements) ) ->orderBy(['[[structureelements.lft]]' => SORT_ASC]) ->all(); - + return [ 'elementType' => Block::class, 'map' => $map, @@ -646,93 +611,92 @@ public function getEagerLoadingMap(array $sourceElements) public function afterSave(bool $isNew) { Neo::$plugin->fields->save($this); - - // If the propagation method just changed, resave all the neo blocks + + // If the propagation method just changed, resave all the neo blocks // TODO - fix the issue when automatically resaving neo fields. - // if ($this->_oldPropagationMethod && $this->propagationMethod !== $this->_oldPropagationMethod) { - // Craft::$app->getQueue()->push(new ResaveElements([ - // 'elementType' => Block::class, - // 'criteria' => [ - // 'fieldId' => $this->id, - // 'siteId' => '*', - // 'unique' => true, - // 'status' => null, - // 'enabledForSite' => false, - // ] - // ])); - // $this->_oldPropagationMethod = null; - // } - + // if ($this->_oldPropagationMethod && $this->propagationMethod !== $this->_oldPropagationMethod) { + // Craft::$app->getQueue()->push(new ResaveElements([ + // 'elementType' => Block::class, + // 'criteria' => [ + // 'fieldId' => $this->id, + // 'siteId' => '*', + // 'unique' => true, + // 'status' => null, + // 'enabledForSite' => false, + // ] + // ])); + // $this->_oldPropagationMethod = null; + // } + parent::afterSave($isNew); } - + /** * @inheritdoc */ public function beforeDelete(): bool { Neo::$plugin->fields->delete($this); - + return parent::beforeDelete(); } - + /** * @inheritdoc */ - -// public function afterElementSave(ElementInterface $element, bool $isNew) -// { -// Neo::$plugin->fields->saveValue($this, $element); -// -// parent::afterElementSave($element, $isNew); -// } - - /** - * @inheritdoc - */ - public function afterElementPropagate(ElementInterface $element, bool $isNew) - { - /** @var Element $element */ - if ($element->duplicateOf !== null) { - Neo::$plugin->fields->duplicateBlocks($this, $element->duplicateOf, $element, true); - } else { - Neo::$plugin->fields->saveValue($this, $element); - } - - // Reset the field value if this is a new element - if ($element->duplicateOf || $isNew) { - $element->setFieldValue($this->handle, null); - } - - parent::afterElementPropagate($element, $isNew); - } - + + // public function afterElementSave(ElementInterface $element, bool $isNew) + // { + // Neo::$plugin->fields->saveValue($this, $element); + // + // parent::afterElementSave($element, $isNew); + // } + + /** + * @inheritdoc + */ + public function afterElementPropagate(ElementInterface $element, bool $isNew) + { + /** @var Element $element */ + if ($element->duplicateOf !== null) { + Neo::$plugin->fields->duplicateBlocks($this, $element->duplicateOf, $element, true); + } else { + Neo::$plugin->fields->saveValue($this, $element); + } + + // Reset the field value if this is a new element + if ($element->duplicateOf || $isNew) { + $element->setFieldValue($this->handle, null); + } + + parent::afterElementPropagate($element, $isNew); + } + /** * @inheritdoc */ public function beforeElementDelete(ElementInterface $element): bool { - if (!parent::beforeElementDelete($element)) - { + if (!parent::beforeElementDelete($element)) { return false; } - + $sitesService = Craft::$app->getSites(); $elementsService = Craft::$app->getElements(); - + // Craft hard-deletes element structure nodes even when soft-deleting an element, which means we lose all Neo // field structure data (i.e. block order, levels) when its owner is soft-deleted. We need to get all block // structures for this field/owner before soft-deleting the blocks, and re-save them after the blocks are // soft-deleted, so the blocks can be restored correctly if the owner element is restored. $blockStructures = []; $blocksBySite = []; - + // Get the structures for each site $structureRows = (new Query()) ->select([ 'id', 'structureId', - 'ownerSiteId', + 'ownerSiteId', 'ownerId', 'fieldId', ]) @@ -742,29 +706,27 @@ public function beforeElementDelete(ElementInterface $element): bool 'ownerId' => $element->id, ]) ->all(); - - foreach ($structureRows as $row) - { + + foreach ($structureRows as $row) { $blockStructures[] = new BlockStructure($row); } - + // Get the blocks for each structure - foreach ($blockStructures as $blockStructure) - { + foreach ($blockStructures as $blockStructure) { // Site IDs start from 1 -- let's treat non-localized blocks as site 0 $key = $blockStructure->ownerSiteId ?? 0; - + $allBlocks = Block::find() ->anyStatus() ->fieldId($this->id) ->owner($element) ->all(); - + $allBlocksCount = count($allBlocks); - + // if the neo block structure doesn't have the ownerSiteId set and has blocks // set the ownerSiteId of the neo block structure. - + // it's set from the first block because we got all blocks related to this structure beforehand // so the siteId should be the same for all blocks. if (empty($blockStructure->ownerSiteId) && $allBlocksCount > 0) { @@ -772,13 +734,12 @@ public function beforeElementDelete(ElementInterface $element): bool // need to set the new key since the ownersiteid is now set $key = $blockStructure->ownerSiteId; } - + $blocksBySite[$key] = $allBlocks; } - + // Delete all Neo blocks for this element and field - foreach ($sitesService->getAllSiteIds() as $siteId) - { + foreach ($sitesService->getAllSiteIds() as $siteId) { $blocks = Block::find() ->anyStatus() ->fieldId($this->id) @@ -786,25 +747,23 @@ public function beforeElementDelete(ElementInterface $element): bool ->owner($element) ->inReverse() ->all(); - - foreach ($blocks as $block) - { + + foreach ($blocks as $block) { $block->deletedWithOwner = true; $elementsService->deleteElement($block); } } - + // Recreate the block structures with the original block data - foreach ($blockStructures as $blockStructure) - { + foreach ($blockStructures as $blockStructure) { $key = $blockStructure->ownerSiteId ?? 0; Neo::$plugin->blocks->saveStructure($blockStructure); Neo::$plugin->blocks->buildStructure($blocksBySite[$key], $blockStructure); } - + return true; } - + /** * @inheritdoc */ @@ -812,10 +771,9 @@ public function afterElementRestore(ElementInterface $element) { $elementsService = Craft::$app->getElements(); $supportedSites = ElementHelper::supportedSitesForElement($element); - + // Restore the Neo blocks that were deleted with $element - foreach ($supportedSites as $supportedSite) - { + foreach ($supportedSites as $supportedSite) { $blocks = Block::find() ->anyStatus() ->siteId($supportedSite['siteId']) @@ -823,13 +781,12 @@ public function afterElementRestore(ElementInterface $element) ->trashed() ->andWhere(['neoblocks.deletedWithOwner' => true]) ->all(); - - foreach ($blocks as $block) - { + + foreach ($blocks as $block) { $elementsService->restoreElement($block); } } - + parent::afterElementRestore($element); } @@ -876,7 +833,7 @@ public function getGqlFragmentEntityByName(string $fragmentName): GqlInlineFragm return $blockType; } - + /** * Returns what current depth the field is nested. * For example, if a Neo field was being rendered inside a Matrix block, its depth will be 2. @@ -888,7 +845,7 @@ private function _getNamespaceDepth() $namespace = Craft::$app->getView()->getNamespace(); return preg_match_all('/\\bfields\\b/', $namespace); } - + /** * Returns the error HTML associated with attempts to nest a Neo field within some other field. * @@ -898,7 +855,7 @@ private function _getNestingErrorHtml(): string { return '' . Craft::t('neo', "Unable to nest Neo fields.") . ''; } - + /** * Returns the input HTML for a Neo field. * @@ -911,31 +868,26 @@ private function _getNestingErrorHtml(): string private function _getInputHtml($value, ElementInterface $element = null, bool $static = false): string { $viewService = Craft::$app->getView(); - - if ($element !== null && $element->hasEagerLoadedElements($this->handle)) - { + + if ($element !== null && $element->hasEagerLoadedElements($this->handle)) { $value = $element->getEagerLoadedElements($this->handle); } - - if ($value instanceof BlockQuery) - { + + if ($value instanceof BlockQuery) { $value = $value->getCachedResult() ?? $value->limit(null)->anyStatus()->all(); } - + $siteId = $element->siteId ?? Craft::$app->getSites()->getCurrentSite()->id; - + $html = ''; - + // Disable Neo fields inside Matrix, SuperTable and potentially other field-grouping field types. - if ($this->_getNamespaceDepth() > 1) - { + if ($this->_getNamespaceDepth() > 1) { $html = $this->_getNestingErrorHtml(); - } - else - { + } else { $viewService->registerAssetBundle(FieldAsset::class); $viewService->registerJs(FieldAsset::createInputJs($this, $value, $static, $siteId)); - + $html = $viewService->renderTemplate('neo/input', [ 'neoField' => $this, 'id' => $viewService->formatInputId($this->handle), @@ -944,10 +896,10 @@ private function _getInputHtml($value, ElementInterface $element = null, bool $s 'static' => $static, ]); } - + return $html; } - + /** * Creates Neo blocks out of the given serialized data. * @@ -957,141 +909,132 @@ private function _getInputHtml($value, ElementInterface $element = null, bool $s */ private function _createBlocksFromSerializedData($value, ElementInterface $element = null): array { - if (!is_array($value)) { - return []; - } - - $requestService = Craft::$app->getRequest(); - + if (!is_array($value)) { + return []; + } + + $blockTypes = ArrayHelper::index(Neo::$plugin->blockTypes->getByFieldId($this->id), 'handle'); + $oldBlocksById = []; + + if ($element && $element->id) { + $ownerId = $element->id; + $blockIds = []; + + foreach (array_keys($value) as $blockId) { + if (is_numeric($blockId) && $blockId !== 0) { + $blockIds[] = $blockId; + + // If that block was duplicated earlier in this request, check for that as well. + if (isset(Elements::$duplicatedElementIds[$blockId])) { + $blockIds[] = Elements::$duplicatedElementIds[$blockId]; + } + } + } + + if (!empty($blockIds)) { + $oldBlocksQuery = Block::find(); + $oldBlocksQuery->fieldId($this->id); + $oldBlocksQuery->ownerId($ownerId); + $oldBlocksQuery->id($blockIds); + $oldBlocksQuery->limit(null); + $oldBlocksQuery->anyStatus(); + $oldBlocksQuery->siteId($element->siteId); + $oldBlocksQuery->indexBy('id'); + $oldBlocksById = $oldBlocksQuery->all(); + } + } else { + $ownerId = null; + } + + // Generally, block data will be received with levels starting from 0, so they need to be adjusted up by 1. + // For entry revisions and new entry drafts, though, the block data will have levels starting from 1. + // Because the first block in a field will always be level 1, we can use that to check whether the count is + // starting from 0 or 1 and thus ensure that all blocks display at the correct level. + $adjustLevels = false; $blocks = []; - - $oldBlocksById = []; - $blockTypes = ArrayHelper::index(Neo::$plugin->blockTypes->getByFieldId($this->id), 'handle'); - $prevBlock = null; - - if ($element && $element->id) - { - $ownerId = $element->id; - $blockIds = []; - - foreach (array_keys($value) as $blockId) - { - if (is_numeric($blockId) && $blockId !== 0) - { - $blockIds[] = $blockId; - - // If that block was duplicated earlier in this request, check for that as well. - if (isset(Elements::$duplicatedElementIds[$blockId])) { - $blockIds[] = Elements::$duplicatedElementIds[$blockId]; - } - } - } - - if (!empty($blockIds)) - { - $oldBlocksQuery = Block::find(); - $oldBlocksQuery->fieldId($this->id); - $oldBlocksQuery->ownerId($ownerId); - $oldBlocksQuery->id($blockIds); - $oldBlocksQuery->limit(null); - $oldBlocksQuery->anyStatus(); - $oldBlocksQuery->siteId($element->siteId); - $oldBlocksQuery->indexBy('id'); - $oldBlocksById = $oldBlocksQuery->all(); - } - } - else - { - $ownerId = null; - } - - // Generally, block data will be received with levels starting from 0, so they need to be adjusted up by 1. - // For entry revisions and new entry drafts, though, the block data will have levels starting from 1. - // Because the first block in a field will always be level 1, we can use that to check whether the count is - // starting from 0 or 1 and thus ensure that all blocks display at the correct level. - $adjustLevels = false; - - if (!empty($value)) - { - $firstBlock = reset($value); - $firstBlockLevel = (int)$firstBlock['level']; - - if ($firstBlockLevel === 0) - { - $adjustLevels = true; - } - } - - foreach ($value as $blockId => $blockData) - { - $blockTypeHandle = isset($blockData['type']) ? $blockData['type'] : null; - $blockType = $blockTypeHandle && isset($blockTypes[$blockTypeHandle]) ? $blockTypes[$blockTypeHandle] : null; - $blockFields = isset($blockData['fields']) ? $blockData['fields'] : null; - - $isEnabled = isset($blockData['enabled']) ? (bool)$blockData['enabled'] : true; - $isCollapsed = isset($blockData['collapsed']) ? (bool)$blockData['collapsed'] : false; - $isModified = isset($blockData['modified']) ? (bool)$blockData['modified'] : false; - $isNew = strpos($blockId, 'new') === 0; - $isDeleted = !isset($oldBlocksById[$blockId]); - - if ($blockType) - { - // Adjust block levels to their correct value if necessary. - $blockLevel = (int)$blockData['level']; - - if ($adjustLevels) - { - $blockLevel++; - } - - if ($isNew || $isDeleted) - { - $block = new Block(); - $block->fieldId = $this->id; - $block->typeId = $blockType->id; - $block->ownerId = $ownerId; - $block->siteId = $element->siteId; - } - else - { - $block = $oldBlocksById[$blockId]; - } - - $block->setOwner($element); - $block->setCollapsed($isCollapsed); - $block->setModified($isModified); - $block->enabled = $isEnabled; - $block->level = $blockLevel; - - $fieldNamespace = $element->getFieldParamNamespace(); - - if ($fieldNamespace !== null) - { - $blockNamespace = ($fieldNamespace ? $fieldNamespace . '.' : '') . "$this->handle.$blockId.fields"; - $block->setFieldParamNamespace($blockNamespace); - } - - if ($blockFields) - { - $block->setFieldValues($blockFields); - } - - if ($prevBlock) - { - $prevBlock->setNext($block); - $block->setPrev($prevBlock); - } - - $prevBlock = $block; - $blocks[] = $block; - } - } - - foreach ($blocks as $block) - { - $block->setAllElements($blocks); - } - + $prevBlock = null; + + if (!empty($value)) { + $firstBlock = reset($value); + $firstBlockLevel = (int)$firstBlock['level']; + + if ($firstBlockLevel === 0) { + $adjustLevels = true; + } + } + + foreach ($value as $blockId => $blockData) { + + if (!isset($blockData['type']) || !isset($blockTypes[$blockData['type']])) { + continue; + } + + $blockType = $blockTypes[$blockData['type']]; + $isEnabled = isset($blockData['enabled']) ? (bool)$blockData['enabled'] : true; + $isCollapsed = isset($blockData['collapsed']) ? (bool)$blockData['collapsed'] : false; + $isModified = isset($blockData['modified']) ? (bool)$blockData['modified'] : false; + + if ( + strpos($blockId, 'new') !== 0 && + !isset($oldBlocksById[$blockId]) && + isset(Elements::$duplicatedElementIds[$blockId]) + ) { + $blockId = Elements::$duplicatedElementIds[$blockId]; + } + + // Is this new? (Or has it been deleted?) + if (strpos($blockId, 'new') === 0 || !isset($oldBlocksById[$blockId])) { + $block = new Block(); + $block->fieldId = $this->id; + $block->typeId = $blockType->id; + $block->ownerId = $ownerId; + $block->siteId = $element->siteId; + } else { + $block = $oldBlocksById[$blockId]; + } + + $blockLevel = (int)$blockData['level']; + + if ($adjustLevels) { + $blockLevel++; + } + + $block->setOwner($element); + $block->setCollapsed($isCollapsed); + $block->setModified($isModified); + $block->enabled = $isEnabled; + $block->level = $blockLevel; + + $fieldNamespace = $element->getFieldParamNamespace(); + + if ($fieldNamespace !== null) { + $blockNamespace = ($fieldNamespace ? $fieldNamespace . '.' : '') . "$this->handle.$blockId.fields"; + $block->setFieldParamNamespace($blockNamespace); + } + + if (isset($blockData['fields'])) { + foreach ($blockData['fields'] as $fieldHandle => $fieldValue) { + try { + $block->setFieldValue($fieldHandle, $fieldValue); + } catch (UnknownPropertyException $e) { + // the field was probably deleted + } + } + } + + if ($prevBlock) { + $prevBlock->setNext($block); + $block->setPrev($prevBlock); + } + + $prevBlock = $block; + $blocks[] = $block; + } + + foreach ($blocks as $block) { + $block->setAllElements($blocks); + } + return $blocks; } } diff --git a/src/elements/db/BlockQuery.php b/src/elements/db/BlockQuery.php index e431aecf..a5a80007 100644 --- a/src/elements/db/BlockQuery.php +++ b/src/elements/db/BlockQuery.php @@ -7,6 +7,7 @@ use Craft; use craft\base\ElementInterface; use craft\db\Query; +use craft\db\Table; use craft\elements\db\ElementQuery; use craft\models\Site; use craft\helpers\Db; @@ -332,8 +333,6 @@ public function useMemoized($use = true) */ protected function beforePrepare(): bool { - -// throw new \Exception(print_r($this, true)); $this->joinElementTable('neoblocks'); $isSaved = $this->id && is_numeric($this->id); @@ -389,6 +388,16 @@ protected function beforePrepare(): bool $this->subQuery->andWhere(Db::parseParam('neoblocks.typeId', $this->typeId)); } + + // Ignore revision/draft blocks by default + if (!$this->id && !$this->ownerId) { + $this->subQuery + ->innerJoin(Table::ELEMENTS . ' owners', '[[owners.id]] = [[neoblocks.ownerId]]') + ->andWhere([ + 'owners.draftId' => null, + 'owners.revisionId' => null, + ]); + } return parent::beforePrepare(); } diff --git a/src/services/Fields.php b/src/services/Fields.php index eaa6ca9f..e08aee93 100644 --- a/src/services/Fields.php +++ b/src/services/Fields.php @@ -234,10 +234,17 @@ public function saveValue(Field $field, ElementInterface $owner) } } - if ($owner->propagateAll && $field->propagationMethod !== Field::PROPAGATION_METHOD_ALL) { + if ( + $field->propagationMethod !== Field::PROPAGATION_METHOD_ALL && + ($owner->propagateAll || !empty($owner->newSiteIds)) + ) { $ownerSiteIds = ArrayHelper::getColumn(ElementHelper::supportedSitesForElement($owner), 'siteId'); $fieldSiteIds = $this->getSupportedSiteIdsForField($field, $owner); $otherSiteIds = array_diff($ownerSiteIds, $fieldSiteIds); + + if (!$owner->propagateAll) { + $otherSiteIds = array_intersect($otherSiteIds, $owner->newSiteIds); + } if (!empty($otherSiteIds)) { // Get the original element and duplicated element for each of those sites @@ -252,7 +259,8 @@ public function saveValue(Field $field, ElementInterface $owner) // Duplicate Matrix blocks, ensuring we don't process the same blocks more than once $handledSiteIds = []; - $cachedQuery = (clone $query)->anyStatus(); + $cachedQuery = clone $query; + $cachedQuery->anyStatus(); $cachedQuery->setCachedResult($blocks); $owner->setFieldValue($field->handle, $cachedQuery); @@ -294,8 +302,14 @@ public function duplicateBlocks(Field $field, ElementInterface $source, ElementI $elementsService = Craft::$app->getElements(); /** @var BlockQuery $query */ $query = $source->getFieldValue($field->handle); + + /** @var Block[] $blocks */ - $blocks = $query->getCachedResult() ?? (clone $query)->anyStatus()->all(); + if (($blocks = $query->getCachedResult()) === null) { + $blocksQuery = clone $query; + $blocks = $blocksQuery->anyStatus()->all(); + } + $newBlockIds = []; $transaction = Craft::$app->getDb()->beginTransaction(); @@ -348,6 +362,7 @@ public function duplicateBlocks(Field $field, ElementInterface $source, ElementI $targetSiteIds = ArrayHelper::getColumn(ElementHelper::supportedSitesForElement($target), 'siteId'); $fieldSiteIds = $this->getSupportedSiteIdsForField($field, $target); $otherSiteIds = array_diff($targetSiteIds, $fieldSiteIds); + if (!empty($otherSiteIds)) { // Get the original element and duplicated element for each of those sites /** @var Element[] $otherSources */ @@ -367,6 +382,7 @@ public function duplicateBlocks(Field $field, ElementInterface $source, ElementI ->anyStatus() ->indexBy('siteId') ->all(); + // Duplicate Matrix blocks, ensuring we don't process the same blocks more than once $handledSiteIds = []; foreach ($otherSources as $otherSource) { @@ -375,13 +391,13 @@ public function duplicateBlocks(Field $field, ElementInterface $source, ElementI continue; } // Make sure we haven't already duplicated blocks for this site, via propagation from another site - if (isset($handledSiteIds[$otherSource->siteId])) { + if (in_array($otherSource->siteId, $handledSiteIds, false)) { continue; } $this->duplicateBlocks($field, $otherSource, $otherTargets[$otherSource->siteId]); // Make sure we don't duplicate blocks for any of the sites that were just propagated to $sourceSupportedSiteIds = $this->getSupportedSiteIdsForField($field, $otherSource); - $handledSiteIds = array_merge($handledSiteIds, array_flip($sourceSupportedSiteIds)); + $handledSiteIds = array_merge($handledSiteIds, $sourceSupportedSiteIds); } } }