From bd5c521bd20244ca17f71f9b3aa5f6fe0b24de78 Mon Sep 17 00:00:00 2001 From: fabianhaef Date: Wed, 1 Jul 2026 21:07:16 +0200 Subject: [PATCH] Fix duplicated Content Block nested fields in element index columns --- CHANGELOG.md | 1 + src/base/Element.php | 73 +++++++++++++++++++++++++++++---- src/services/ElementSources.php | 18 ++++++-- 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4071c85f8c0..0f20091fa05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Fixed a bug where Content Block fields’ nested fields could be listed multiple times in element index column settings, when the same field was used by multiple entry types. ([#19197](https://github.com/craftcms/cms/pull/19197)) - Fixed a bug where the `site`/`siteId` params weren’t being respected on eager-loaded `localized` queries. ([#18588](https://github.com/craftcms/cms/issues/18588)) - Fixed a bug where it wasn’t possible to enter decimal values after tabbing into a Money field. ([#19156](https://github.com/craftcms/cms/issues/19156)) - Fixed a bug where the search inputs on the Entry Types and Fields settings index pages were case-sensitive on PostgreSQL. ([#19158](https://github.com/craftcms/cms/issues/19158)) diff --git a/src/base/Element.php b/src/base/Element.php index cd1450368c0..1bf10479020 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -6238,16 +6238,18 @@ private function contentBlockAttributeHtml(string $attribute): string { $parts = explode('.', $attribute); $uid = StringHelper::removeLeft(array_shift($parts), 'contentBlock:'); - $layoutElement = $this->getFieldLayout()?->getElementByUid($uid); - if (!$layoutElement instanceof CustomField) { - return ''; + $field = null; + $layoutElement = $this->getFieldLayout()?->getElementByUid($uid); + if ($layoutElement instanceof CustomField) { + try { + $field = $layoutElement->getField(); + } catch (FieldNotFoundException) { + } } - try { - $field = $layoutElement->getField(); - } catch (FieldNotFoundException) { - return ''; + if (!$field instanceof ContentBlockField) { + $field = $this->_contentBlockFieldFromLayoutElementUid($uid); } if (!$field instanceof ContentBlockField) { @@ -6255,7 +6257,62 @@ private function contentBlockAttributeHtml(string $attribute): string } $block = $this->getFieldValue($field->handle); - return $block->getAttributeHtml(implode('.', $parts)); + return $block?->getAttributeHtml(implode('.', $parts)) ?? ''; + } + + /** + * Returns the Content Block field referenced by a table attribute key. + * + * Content Block table attributes are deduplicated across the layouts of an element source (see + * [[\craft\services\ElementSources::getTableAttributesForFieldLayouts()]]), so the layout element + * UID in the key may belong to a different entry type’s layout than this element’s. This resolves + * it to the matching instance — by field UID and effective handle — in this element’s own layout. + * + * (Separate from [[_getFieldFromAlternativeLayouts()]], which matches on the raw handle override + * and so can’t resolve Content Blocks that keep their default handle.) + * + * @param string $layoutElementUid + * @return ContentBlockField|null + */ + private function _contentBlockFieldFromLayoutElementUid(string $layoutElementUid): ?ContentBlockField + { + // Find the Content Block field + effective handle that the UID refers to, + // in any of this element type’s layouts + $fieldUid = null; + $handle = null; + foreach (Craft::$app->getFields()->getLayoutsByType(static::class) as $fieldLayout) { + $layoutElement = $fieldLayout->getElementByUid($layoutElementUid); + if (!$layoutElement instanceof CustomField) { + continue; + } + try { + $field = $layoutElement->getField(); + } catch (FieldNotFoundException) { + continue; + } + if ($field instanceof ContentBlockField) { + $fieldUid = $field->uid; + $handle = $field->handle; + } + break; + } + + if ($fieldUid === null) { + return null; + } + + // Return the matching Content Block instance in this element’s own layout + foreach ($this->getFieldLayout()?->getCustomFields() ?? [] as $field) { + if ( + $field instanceof ContentBlockField && + $field->uid === $fieldUid && + $field->handle === $handle + ) { + return $field; + } + } + + return null; } private function generatedFieldAttributeHtml(string $attribute): string diff --git a/src/services/ElementSources.php b/src/services/ElementSources.php index 7c636284b1c..36d70ede819 100644 --- a/src/services/ElementSources.php +++ b/src/services/ElementSources.php @@ -643,6 +643,8 @@ public function getTableAttributesForFieldLayouts(array $fieldLayouts): array /** @var CustomField[][] $groupedFieldElements */ $groupedFieldElements = []; $groupedFieldInstances = []; + /** @var CustomField[] $groupedContentBlocks */ + $groupedContentBlocks = []; foreach ($fieldLayouts as $fieldLayout) { foreach ($fieldLayout->getTabs() as $tab) { @@ -667,9 +669,11 @@ public function getTableAttributesForFieldLayouts(array $fieldLayouts): array (!$user || $user->admin || ($layoutElement->getUserCondition()?->matchElement($user) ?? true)) ) { if ($field instanceof ContentBlock) { - foreach ($this->getTableAttributesForFieldLayouts([$field->getFieldLayout()]) as $key => $attribute) { - $attributes["contentBlock:{$field->layoutElement->uid}.$key"] = $attribute; - } + // Combine it with any other instances of the same Content Block field + // (from other layouts) that share the same handle, so its nested fields + // are only listed once + $key = $field->uid . ' - ' . ($layoutElement->handle ?? $field->handle); + $groupedContentBlocks[$key] ??= $layoutElement; } elseif ($layoutElement->handle === null) { // The handle wasn't overridden, so combine it with any other instances (from other layouts) // where the handle also wasn't overridden @@ -696,6 +700,14 @@ public function getTableAttributesForFieldLayouts(array $fieldLayouts): array } } + foreach ($groupedContentBlocks as $layoutElement) { + /** @var ContentBlock $field */ + $field = $layoutElement->getField(); + foreach ($this->getTableAttributesForFieldLayouts([$field->getFieldLayout()]) as $key => $attribute) { + $attributes["contentBlock:$layoutElement->uid.$key"] = $attribute; + } + } + foreach ($groupedFieldElements as $fieldElements) { $field = $fieldElements[0]->getField(); $labels = array_unique(array_map(fn(CustomField $layoutElement) => $layoutElement->label(), $fieldElements));