From bc6bd5ce6626447b00a0214da65521b475701f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 11 Apr 2025 08:53:04 -0400 Subject: [PATCH 01/17] Define exposed slots schema --- config/schema/experience_builder.schema.yml | 56 +++++++++++++++++++ src/Entity/ContentTemplate.php | 3 + .../Config/ContentTemplateValidationTest.php | 11 ++++ 3 files changed, 70 insertions(+) diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index f25ec15bf1..641158f2a3 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -547,3 +547,59 @@ experience_builder.content_template.*.*.*: - Drupal\Core\Block\TitleBlockPluginInterface - Drupal\Core\Block\MessagesBlockPluginInterface presence: ~ + +# Exposed slots are only supported by the "canonical" view mode, which is generally +# `full`. Example: +# +# exposed_slots: +# profile_bio: +# label: 'Profile Bio!' +# component_uuid: 28bcab26-e434-4ad4-9eaf-0520bdb32fcc +# slot_name: column_two +# intro: +# label: 'Intro' +# component_uuid: 98bcab26-e434-4ad4-9eaf-0520bdb32fcc +# slot_name: body +# +# @see \Drupal\Core\Entity\Controller\EntityViewController::view() +# +# @todo Core entity types ship with a `full` view mode, but it's not guaranteed to +# exist for every content entity type. Ensure XB creates it when opting a content +# entity bundle into XB rendering in https://www.drupal.org/i/3518248. +experience_builder.content_template.*.*.full: + type: experience_builder.content_template.*.*.* + constraints: + FullyValidatable: ~ + mapping: + exposed_slots: + type: sequence + # This is ordered by key because the keys are the machine names assigned to the + # exposed slots. These machine names are, essentially, "aliases" for a particular + # slot in a particular component in the component tree. + orderby: key + # If this sequence is empty, the site builder that crafted this template has not + # exposed any slots to content creators, which means there's nothing for content + # creators to do other than enter values for the content entity form (under the + # "Page Data" tab in the designs). + sequence: + type: mapping + mapping: + # The UUID of the component that contains the slot being exposed. This CANNOT + # be the same as \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure::ROOT_UUID, + # because that would mean the entire template could be overridden by an individual + # content entity, which is not an appropriate use of a content template (we have + # a whole distinct landing page content entity type for that). + # @todo Validate that: + # - This is not the root UUID + # - This component actually exists in the component tree + # - The slot we're exposing is empty. + component_uuid: + type: uuid + label: 'UUID of the component instance that contains the exposed slot' + # @todo Validate that the slot name is actually defined by the component. + slot_name: + type: string + label: 'The name of the slot exposed slot, as known to the component' + label: + type: required_label + label: 'A human-readable label for the exposed slot' diff --git a/src/Entity/ContentTemplate.php b/src/Entity/ContentTemplate.php index 2672b4beb7..410f5a399e 100644 --- a/src/Entity/ContentTemplate.php +++ b/src/Entity/ContentTemplate.php @@ -53,6 +53,7 @@ use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItemInstantiat 'content_entity_type_bundle', 'content_entity_type_view_mode', 'component_tree', + 'exposed_slots', ], )] final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEntityInterface, EntityViewDisplayInterface { @@ -98,6 +99,8 @@ final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEnt */ protected ?array $component_tree; + protected array $exposed_slots = []; + /** * Tries to load a template for a particular entity, in a specific view mode. * diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 48736f4f66..134665fe25 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -357,4 +357,15 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ]); } + public function testRootUuidCannotBeExposed(): void { + $this->entity->set('exposed_slots', [ + 'not_allowed' => [ + 'component_uuid' => ComponentTreeStructure::ROOT_UUID, + 'slot_name' => 'not-a-thing', + 'label' => "This won't work", + ], + ]); + $this->assertValidationErrors([]); + } + } -- GitLab From c56dcda481667e17dffb0b8ea9f4f79d5e770af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 11 Apr 2025 09:31:31 -0400 Subject: [PATCH 02/17] Set up the validation test cases --- config/schema/experience_builder.schema.yml | 5 -- .../Config/ContentTemplateValidationTest.php | 79 ++++++++++++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index 641158f2a3..b169d74e90 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -589,14 +589,9 @@ experience_builder.content_template.*.*.full: # because that would mean the entire template could be overridden by an individual # content entity, which is not an appropriate use of a content template (we have # a whole distinct landing page content entity type for that). - # @todo Validate that: - # - This is not the root UUID - # - This component actually exists in the component tree - # - The slot we're exposing is empty. component_uuid: type: uuid label: 'UUID of the component instance that contains the exposed slot' - # @todo Validate that the slot name is actually defined by the component. slot_name: type: string label: 'The name of the slot exposed slot, as known to the component' diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 134665fe25..931dd48060 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -365,7 +365,84 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'label' => "This won't work", ], ]); - $this->assertValidationErrors([]); + $this->fail('Validate that the root UUID is not exposed.'); + } + + public function testExposedSlotComponentMustExistInTree(): void { + $this->entity->set('exposed_slots', [ + 'not_a_thing' => [ + 'component_uuid' => 'not-in-the-tree', + 'slot_name' => 'not-a-thing', + 'label' => "Can't expose a slot in a component we don't have!", + ], + ]); + $this->fail('Validate that the component with the exposed slot exists in the tree.'); + } + + public function testExposedSlotMustBeEmpty(): void { + $this->entity->set('component_tree', [ + 'tree' => self::encodeXBData([ + ComponentTreeStructure::ROOT_UUID => [ + ['uuid' => 'has-exposed-slot', 'component' => 'sdc.xb_test_sdc.props-slots'], + ], + 'has-exposed-slot' => [ + 'the_footer' => [ + ['uuid' => 'greeting', 'component' => 'sdc.xb_test_sdc.props-no-slots'], + ], + ], + ]), + 'inputs' => self::encodeXBData([ + 'has-exposed-slot' => [ + 'heading' => [ + 'sourceType' => 'static:field_item:string', + 'value' => "My footer is showing", + 'expression' => 'ℹ︎string␟value', + ], + ], + 'greeting' => [ + 'heading' => [ + 'sourceType' => 'static:field_item:string', + 'value' => "I shouldn't be here", + 'expression' => 'ℹ︎string␟value', + ], + ], + ]), + ]); + $this->entity->set('exposed_slots', [ + 'filled_footer' => [ + 'component_uuid' => 'has-exposed-slot', + 'slot_name' => 'the_footer', + 'label' => "Something's already here!", + ], + ]); + $this->fail('Validate that the exposed slot must be empty.'); + } + + public function testExposedSlotMustBeDefinedByComponent(): void { + $this->entity->set('component_tree', [ + 'tree' => self::encodeXBData([ + ComponentTreeStructure::ROOT_UUID => [ + ['uuid' => 'has-exposed-slot', 'component' => 'sdc.xb_test_sdc.props-slots'], + ], + ]), + 'inputs' => self::encodeXBData([ + 'has-exposed-slot' => [ + 'heading' => [ + 'sourceType' => 'static:field_item:string', + 'value' => "My footer is showing", + 'expression' => 'ℹ︎string␟value', + ], + ], + ]), + ]); + $this->entity->set('exposed_slots', [ + 'filled_footer' => [ + 'component_uuid' => 'has-exposed-slot', + 'slot_name' => 'not_a_real_slot', + 'label' => "Whither this slot you speak of?", + ], + ]); + $this->fail('Validate that the exposed slot is defined by the component.'); } } -- GitLab From a190e60029720a2247a5f42a645754fa01886e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 11 Apr 2025 09:58:56 -0400 Subject: [PATCH 03/17] Start validating --- config/schema/experience_builder.schema.yml | 50 ++++++++++--------- .../Constraint/ValidExposedSlotConstraint.php | 21 ++++++++ .../ValidExposedSlotConstraintValidator.php | 40 +++++++++++++++ .../Config/ContentTemplateValidationTest.php | 10 ++-- 4 files changed, 94 insertions(+), 27 deletions(-) create mode 100644 src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php create mode 100644 src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index b169d74e90..dbdf29efba 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -547,30 +547,6 @@ experience_builder.content_template.*.*.*: - Drupal\Core\Block\TitleBlockPluginInterface - Drupal\Core\Block\MessagesBlockPluginInterface presence: ~ - -# Exposed slots are only supported by the "canonical" view mode, which is generally -# `full`. Example: -# -# exposed_slots: -# profile_bio: -# label: 'Profile Bio!' -# component_uuid: 28bcab26-e434-4ad4-9eaf-0520bdb32fcc -# slot_name: column_two -# intro: -# label: 'Intro' -# component_uuid: 98bcab26-e434-4ad4-9eaf-0520bdb32fcc -# slot_name: body -# -# @see \Drupal\Core\Entity\Controller\EntityViewController::view() -# -# @todo Core entity types ship with a `full` view mode, but it's not guaranteed to -# exist for every content entity type. Ensure XB creates it when opting a content -# entity bundle into XB rendering in https://www.drupal.org/i/3518248. -experience_builder.content_template.*.*.full: - type: experience_builder.content_template.*.*.* - constraints: - FullyValidatable: ~ - mapping: exposed_slots: type: sequence # This is ordered by key because the keys are the machine names assigned to the @@ -583,6 +559,8 @@ experience_builder.content_template.*.*.full: # "Page Data" tab in the designs). sequence: type: mapping + constraints: + ValidExposedSlot: ~ mapping: # The UUID of the component that contains the slot being exposed. This CANNOT # be the same as \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure::ROOT_UUID, @@ -598,3 +576,27 @@ experience_builder.content_template.*.*.full: label: type: required_label label: 'A human-readable label for the exposed slot' + +# Exposed slots are only supported by the "canonical" view mode, which is generally +# `full`. Example: +# +# exposed_slots: +# profile_bio: +# label: 'Profile Bio!' +# component_uuid: 28bcab26-e434-4ad4-9eaf-0520bdb32fcc +# slot_name: column_two +# intro: +# label: 'Intro' +# component_uuid: 98bcab26-e434-4ad4-9eaf-0520bdb32fcc +# slot_name: body +# +# @see \Drupal\Core\Entity\Controller\EntityViewController::view() +# +# @todo Core entity types ship with a `full` view mode, but it's not guaranteed to +# exist for every content entity type. Ensure XB creates it when opting a content +# entity bundle into XB rendering in https://www.drupal.org/i/3518248. +#experience_builder.content_template.*.*.full: +# type: experience_builder.content_template.*.*.* +# constraints: +# FullyValidatable: ~ +# mapping: diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php new file mode 100644 index 0000000000..04b8736b21 --- /dev/null +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\experience_builder\Plugin\Validation\Constraint; + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Validation\Attribute\Constraint; +use Symfony\Component\Validator\Constraint as SymfonyConstraint; + +#[Constraint( + id: 'ValidExposedSlot', + label: new TranslatableMarkup('Validates an exposed slot', [], ['context' => 'Validation']), +)] +final class ValidExposedSlotConstraint extends SymfonyConstraint { + + public string $rootExposedMessage = 'Exposing the full component tree is not allowed.'; + + public string $unknownComponentMessage = 'The component %id does not exist in the tree.'; + +} diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php new file mode 100644 index 0000000000..cb6c49f962 --- /dev/null +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\experience_builder\Plugin\Validation\Constraint; + +use Drupal\experience_builder\Entity\ComponentTreeEntityInterface; +use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * Validates the `ValidExposedSlot` constraint. + */ +final class ValidExposedSlotConstraintValidator extends ConstraintValidator { + + /** + * {@inheritdoc} + */ + public function validate(mixed $value, Constraint $constraint): void { + assert($constraint instanceof ValidExposedSlotConstraint); + + assert(is_array($value)); + if ($value['component_uuid'] === ComponentTreeStructure::ROOT_UUID) { + $this->context->addViolation($constraint->rootExposedMessage); + } + + $root = $this->context->getRoot()->getEntity(); + assert($root instanceof ComponentTreeEntityInterface); + /** @var \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure $tree */ + $tree = $root->getComponentTree()->get('tree'); + + if (!in_array($value['component_uuid'], $tree->getComponentInstanceUuids(), TRUE)) { + $this->context->addViolation($constraint->unknownComponentMessage, [ + '%id' => $value['component_uuid'], + ]); + } + } + +} diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 931dd48060..c7d7325d40 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -365,18 +365,22 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'label' => "This won't work", ], ]); - $this->fail('Validate that the root UUID is not exposed.'); + $this->assertValidationErrors([ + 'exposed_slots.not_allowed' => 'Exposing the full component tree is not allowed.', + ]); } public function testExposedSlotComponentMustExistInTree(): void { $this->entity->set('exposed_slots', [ 'not_a_thing' => [ - 'component_uuid' => 'not-in-the-tree', + 'component_uuid' => 'f0a5a653-ee1c-4469-903a-119d70edbf02', 'slot_name' => 'not-a-thing', 'label' => "Can't expose a slot in a component we don't have!", ], ]); - $this->fail('Validate that the component with the exposed slot exists in the tree.'); + $this->assertValidationErrors([ + 'exposed_slots.not_a_thing' => 'The component <em class="placeholder">f0a5a653-ee1c-4469-903a-119d70edbf02</em> does not exist in the tree.', + ]); } public function testExposedSlotMustBeEmpty(): void { -- GitLab From 6d4b55fcd4e42f301848d099600086ba2423ff24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 11 Apr 2025 10:35:08 -0400 Subject: [PATCH 04/17] Ensure slot actually exists --- .../Constraint/ValidExposedSlotConstraint.php | 2 ++ .../ValidExposedSlotConstraintValidator.php | 13 +++++++++++++ .../Config/ContentTemplateValidationTest.php | 15 ++++++++------- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php index 04b8736b21..6e422d881f 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php @@ -18,4 +18,6 @@ final class ValidExposedSlotConstraint extends SymfonyConstraint { public string $unknownComponentMessage = 'The component %id does not exist in the tree.'; + public string $undefinedSlotMessage = 'The component %id does not have a %slot slot.'; + } diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php index cb6c49f962..2a3d42d10d 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\experience_builder\Plugin\Validation\Constraint; +use Drupal\experience_builder\ComponentSource\ComponentSourceWithSlotsInterface; use Drupal\experience_builder\Entity\ComponentTreeEntityInterface; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; use Symfony\Component\Validator\Constraint; @@ -35,6 +36,18 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { '%id' => $value['component_uuid'], ]); } + + $source = $tree->getComponentSource($value['component_uuid']); + $slot_exists = FALSE; + if ($source instanceof ComponentSourceWithSlotsInterface) { + $slot_exists = array_key_exists($value['slot_name'], $source->getSlotDefinitions()); + } + if ($slot_exists === FALSE) { + $this->context->addViolation($constraint->undefinedSlotMessage, [ + '%id' => $value['component_uuid'], + '%slot' => $value['slot_name'], + ]); + } } } diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index c7d7325d40..3d26c6bd0e 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -426,27 +426,28 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe $this->entity->set('component_tree', [ 'tree' => self::encodeXBData([ ComponentTreeStructure::ROOT_UUID => [ - ['uuid' => 'has-exposed-slot', 'component' => 'sdc.xb_test_sdc.props-slots'], + ['uuid' => '20398504-ce6f-4a79-819e-405ea34eefc4', 'component' => 'sdc.xb_test_sdc.props-slots'], ], ]), 'inputs' => self::encodeXBData([ - 'has-exposed-slot' => [ + '20398504-ce6f-4a79-819e-405ea34eefc4' => [ 'heading' => [ - 'sourceType' => 'static:field_item:string', - 'value' => "My footer is showing", - 'expression' => 'ℹ︎string␟value', + 'sourceType' => 'dynamic', + 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', ], ], ]), ]); $this->entity->set('exposed_slots', [ 'filled_footer' => [ - 'component_uuid' => 'has-exposed-slot', + 'component_uuid' => '20398504-ce6f-4a79-819e-405ea34eefc4', 'slot_name' => 'not_a_real_slot', 'label' => "Whither this slot you speak of?", ], ]); - $this->fail('Validate that the exposed slot is defined by the component.'); + $this->assertValidationErrors([ + 'exposed_slots.filled_footer' => 'The component <em class="placeholder">20398504-ce6f-4a79-819e-405ea34eefc4</em> does not have a <em class="placeholder">not_a_real_slot</em> slot.', + ]); } } -- GitLab From c69688940ebbed5ef75b8e2b53322f839f1ef45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 11 Apr 2025 11:02:28 -0400 Subject: [PATCH 05/17] Finish validation of the exposed slot --- .../ComponentSourceInterface.php | 4 ++++ .../Constraint/ValidExposedSlotConstraint.php | 2 ++ .../ValidExposedSlotConstraintValidator.php | 18 +++++++++++++++++- .../Config/ContentTemplateValidationTest.php | 17 +++++++++-------- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/ComponentSource/ComponentSourceInterface.php b/src/ComponentSource/ComponentSourceInterface.php index 15b9a0982e..b2fb824699 100644 --- a/src/ComponentSource/ComponentSourceInterface.php +++ b/src/ComponentSource/ComponentSourceInterface.php @@ -31,11 +31,15 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; * - a block plugin component source — which renders the a block and needs * settings for the block plugin * + * Not all component sources support slots. A source that supports slots should + * implement \Drupal\experience_builder\ComponentSource\ComponentSourceWithSlotsInterface. + * * @phpstan-import-type PropSourceArray from \Drupal\experience_builder\PropSource\PropSourceBase * * @see \Drupal\experience_builder\Attribute\ComponentSource * @see \Drupal\experience_builder\ComponentSource\ComponentSourceBase * @see \Drupal\experience_builder\ComponentSource\ComponentSourceManager + * @see \Drupal\experience_builder\ComponentSource\ComponentSourceWithSlotsInterface */ interface ComponentSourceInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurableInterface, PluginFormInterface, DependentPluginInterface, ContextAwarePluginInterface { diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php index 6e422d881f..a706ac247f 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php @@ -18,6 +18,8 @@ final class ValidExposedSlotConstraint extends SymfonyConstraint { public string $unknownComponentMessage = 'The component %id does not exist in the tree.'; + public string $slotNotEmptyMessage = 'The %slot slot must be empty.'; + public string $undefinedSlotMessage = 'The component %id does not have a %slot slot.'; } diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php index 2a3d42d10d..c3cdb440e6 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php @@ -9,6 +9,7 @@ use Drupal\experience_builder\Entity\ComponentTreeEntityInterface; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validates the `ValidExposedSlot` constraint. @@ -21,7 +22,9 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { public function validate(mixed $value, Constraint $constraint): void { assert($constraint instanceof ValidExposedSlotConstraint); - assert(is_array($value)); + assert(is_array($value), new UnexpectedTypeException($value, 'array')); + + // The root UUID (i.e., the entire component tree) cannot be exposed. if ($value['component_uuid'] === ComponentTreeStructure::ROOT_UUID) { $this->context->addViolation($constraint->rootExposedMessage); } @@ -31,12 +34,15 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { /** @var \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure $tree */ $tree = $root->getComponentTree()->get('tree'); + // The component that contains the exposed slot actually has to, y'know, be + // in the tree somewhere. if (!in_array($value['component_uuid'], $tree->getComponentInstanceUuids(), TRUE)) { $this->context->addViolation($constraint->unknownComponentMessage, [ '%id' => $value['component_uuid'], ]); } + // The component has to actually define the slot being exposed. $source = $tree->getComponentSource($value['component_uuid']); $slot_exists = FALSE; if ($source instanceof ComponentSourceWithSlotsInterface) { @@ -48,6 +54,16 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { '%slot' => $value['slot_name'], ]); } + + // The exposed slot has to be empty. + foreach ($tree->getSlotChildrenDepthFirst() as $parent_uuid => $child) { + if ($parent_uuid === $value['component_uuid']) { + $this->context->addViolation($constraint->slotNotEmptyMessage, [ + '%slot' => $child['slot'], + ]); + break; + } + } } } diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 3d26c6bd0e..4c4af7e894 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -387,16 +387,16 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe $this->entity->set('component_tree', [ 'tree' => self::encodeXBData([ ComponentTreeStructure::ROOT_UUID => [ - ['uuid' => 'has-exposed-slot', 'component' => 'sdc.xb_test_sdc.props-slots'], + ['uuid' => '67062b9a-b35f-453f-a66b-fdb9988b604b', 'component' => 'sdc.xb_test_sdc.props-slots'], ], - 'has-exposed-slot' => [ + '67062b9a-b35f-453f-a66b-fdb9988b604b' => [ 'the_footer' => [ ['uuid' => 'greeting', 'component' => 'sdc.xb_test_sdc.props-no-slots'], ], ], ]), 'inputs' => self::encodeXBData([ - 'has-exposed-slot' => [ + '67062b9a-b35f-453f-a66b-fdb9988b604b' => [ 'heading' => [ 'sourceType' => 'static:field_item:string', 'value' => "My footer is showing", @@ -405,21 +405,22 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ], 'greeting' => [ 'heading' => [ - 'sourceType' => 'static:field_item:string', - 'value' => "I shouldn't be here", - 'expression' => 'ℹ︎string␟value', + 'sourceType' => 'dynamic', + 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', ], ], ]), ]); $this->entity->set('exposed_slots', [ 'filled_footer' => [ - 'component_uuid' => 'has-exposed-slot', + 'component_uuid' => '67062b9a-b35f-453f-a66b-fdb9988b604b', 'slot_name' => 'the_footer', 'label' => "Something's already here!", ], ]); - $this->fail('Validate that the exposed slot must be empty.'); + $this->assertValidationErrors([ + 'exposed_slots.filled_footer' => 'The <em class="placeholder">the_footer</em> slot must be empty.', + ]); } public function testExposedSlotMustBeDefinedByComponent(): void { -- GitLab From 0e064ae3e21daa8fa2131f37d5aa7a5f1e9e8159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 11 Apr 2025 12:17:39 -0400 Subject: [PATCH 06/17] Fix ContentTemplateValidationTest --- src/Entity/ContentTemplate.php | 7 ++++++- .../ValidExposedSlotConstraintValidator.php | 19 +++++++++++-------- .../Config/ContentTemplateValidationTest.php | 5 ++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Entity/ContentTemplate.php b/src/Entity/ContentTemplate.php index 410f5a399e..b3fcdd0fde 100644 --- a/src/Entity/ContentTemplate.php +++ b/src/Entity/ContentTemplate.php @@ -99,7 +99,12 @@ final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEnt */ protected ?array $component_tree; - protected array $exposed_slots = []; + /** + * The exposed slots. + * + * @var ?array<string, array{'component_uuid': string, 'slot_name': string, 'label': string}> + */ + protected ?array $exposed_slots = []; /** * Tries to load a template for a particular entity, in a specific view mode. diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php index c3cdb440e6..c6baa3409d 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php @@ -34,20 +34,23 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { /** @var \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure $tree */ $tree = $root->getComponentTree()->get('tree'); - // The component that contains the exposed slot actually has to, y'know, be - // in the tree somewhere. - if (!in_array($value['component_uuid'], $tree->getComponentInstanceUuids(), TRUE)) { + $slot_exists = FALSE; + try { + $source = $tree->getComponentSource($value['component_uuid']); + if ($source instanceof ComponentSourceWithSlotsInterface) { + $slot_exists = array_key_exists($value['slot_name'], $source->getSlotDefinitions()); + } + } + catch (\OutOfRangeException) { + // The component that contains the exposed slot isn't in the tree at all, + // so there's nothing else for us to do. $this->context->addViolation($constraint->unknownComponentMessage, [ '%id' => $value['component_uuid'], ]); + return; } // The component has to actually define the slot being exposed. - $source = $tree->getComponentSource($value['component_uuid']); - $slot_exists = FALSE; - if ($source instanceof ComponentSourceWithSlotsInterface) { - $slot_exists = array_key_exists($value['slot_name'], $source->getSlotDefinitions()); - } if ($slot_exists === FALSE) { $this->context->addViolation($constraint->undefinedSlotMessage, [ '%id' => $value['component_uuid'], diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 4c4af7e894..01d65b9266 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -366,7 +366,10 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ], ]); $this->assertValidationErrors([ - 'exposed_slots.not_allowed' => 'Exposing the full component tree is not allowed.', + 'exposed_slots.not_allowed' => [ + 'Exposing the full component tree is not allowed.', + 'The component <em class="placeholder">a548b48d-58a8-4077-aa04-da9405a6f418</em> does not exist in the tree.', + ], ]); } -- GitLab From 96941258f8f2e04301dabc31dec07f5d2cf1134c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 15 Apr 2025 14:30:13 -0400 Subject: [PATCH 07/17] Minor feedback additions --- src/Entity/ContentTemplate.php | 2 +- .../Validation/Constraint/ValidExposedSlotConstraint.php | 4 +++- .../Constraint/ValidExposedSlotConstraintValidator.php | 1 + tests/src/Kernel/Config/ContentTemplateValidationTest.php | 5 +++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Entity/ContentTemplate.php b/src/Entity/ContentTemplate.php index b3fcdd0fde..807890e4bc 100644 --- a/src/Entity/ContentTemplate.php +++ b/src/Entity/ContentTemplate.php @@ -104,7 +104,7 @@ final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEnt * * @var ?array<string, array{'component_uuid': string, 'slot_name': string, 'label': string}> */ - protected ?array $exposed_slots = []; + protected array $exposed_slots = []; /** * Tries to load a template for a particular entity, in a specific view mode. diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php index a706ac247f..c6059dd93d 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php @@ -9,11 +9,13 @@ use Drupal\Core\Validation\Attribute\Constraint; use Symfony\Component\Validator\Constraint as SymfonyConstraint; #[Constraint( - id: 'ValidExposedSlot', + id: self::PLUGIN_ID, label: new TranslatableMarkup('Validates an exposed slot', [], ['context' => 'Validation']), )] final class ValidExposedSlotConstraint extends SymfonyConstraint { + public const string PLUGIN_ID = 'ValidExposedSlot'; + public string $rootExposedMessage = 'Exposing the full component tree is not allowed.'; public string $unknownComponentMessage = 'The component %id does not exist in the tree.'; diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php index c6baa3409d..960fe2c1b8 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php @@ -27,6 +27,7 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { // The root UUID (i.e., the entire component tree) cannot be exposed. if ($value['component_uuid'] === ComponentTreeStructure::ROOT_UUID) { $this->context->addViolation($constraint->rootExposedMessage); + return; } $root = $this->context->getRoot()->getEntity(); diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 01d65b9266..7ff49f8303 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -29,6 +29,11 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe */ protected bool $hasLabel = FALSE; + /** + * {@inheritdoc} + */ + protected static array $propertiesWithOptionalValues = ['exposed_slots']; + /** * {@inheritdoc} */ -- GitLab From b49a1ade395bb08bd0af93f6a483f9712f2b1d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 15 Apr 2025 15:10:11 -0400 Subject: [PATCH 08/17] Validate view mode slot --- config/schema/experience_builder.schema.yml | 44 ++++++++----------- .../Constraint/ValidExposedSlotConstraint.php | 16 +++++++ .../ValidExposedSlotConstraintValidator.php | 10 ++++- .../Config/ContentTemplateValidationTest.php | 33 ++++++++++++++ 4 files changed, 76 insertions(+), 27 deletions(-) diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index dbdf29efba..d81df6ecef 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -560,7 +560,25 @@ experience_builder.content_template.*.*.*: sequence: type: mapping constraints: - ValidExposedSlot: ~ + # Exposed slots are only supported by the "canonical" view mode, which is generally + # `full`. Example: + # + # exposed_slots: + # profile_bio: + # label: 'Profile Bio!' + # component_uuid: 28bcab26-e434-4ad4-9eaf-0520bdb32fcc + # slot_name: column_two + # intro: + # label: 'Intro' + # component_uuid: 98bcab26-e434-4ad4-9eaf-0520bdb32fcc + # slot_name: body + # + # @see \Drupal\Core\Entity\Controller\EntityViewController::view() + # + # @todo Core entity types ship with a `full` view mode, but it's not guaranteed to + # exist for every content entity type. Ensure XB creates it when opting a content + # entity bundle into XB rendering in https://www.drupal.org/i/3518248. + ValidExposedSlot: full mapping: # The UUID of the component that contains the slot being exposed. This CANNOT # be the same as \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure::ROOT_UUID, @@ -576,27 +594,3 @@ experience_builder.content_template.*.*.*: label: type: required_label label: 'A human-readable label for the exposed slot' - -# Exposed slots are only supported by the "canonical" view mode, which is generally -# `full`. Example: -# -# exposed_slots: -# profile_bio: -# label: 'Profile Bio!' -# component_uuid: 28bcab26-e434-4ad4-9eaf-0520bdb32fcc -# slot_name: column_two -# intro: -# label: 'Intro' -# component_uuid: 98bcab26-e434-4ad4-9eaf-0520bdb32fcc -# slot_name: body -# -# @see \Drupal\Core\Entity\Controller\EntityViewController::view() -# -# @todo Core entity types ship with a `full` view mode, but it's not guaranteed to -# exist for every content entity type. Ensure XB creates it when opting a content -# entity bundle into XB rendering in https://www.drupal.org/i/3518248. -#experience_builder.content_template.*.*.full: -# type: experience_builder.content_template.*.*.* -# constraints: -# FullyValidatable: ~ -# mapping: diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php index c6059dd93d..a71b7e7682 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraint.php @@ -16,6 +16,13 @@ final class ValidExposedSlotConstraint extends SymfonyConstraint { public const string PLUGIN_ID = 'ValidExposedSlot'; + /** + * The view mode in which the exposed slots will be used. + * + * @var string + */ + public string $viewMode = 'full'; + public string $rootExposedMessage = 'Exposing the full component tree is not allowed.'; public string $unknownComponentMessage = 'The component %id does not exist in the tree.'; @@ -24,4 +31,13 @@ final class ValidExposedSlotConstraint extends SymfonyConstraint { public string $undefinedSlotMessage = 'The component %id does not have a %slot slot.'; + public string $viewModeMismatchMessage = 'Exposed slots are only allowed in the %mode view mode.'; + + /** + * {@inheritdoc} + */ + public function getDefaultOption(): string { + return 'viewMode'; + } + } diff --git a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php index 960fe2c1b8..355859e6b0 100644 --- a/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidExposedSlotConstraintValidator.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Drupal\experience_builder\Plugin\Validation\Constraint; use Drupal\experience_builder\ComponentSource\ComponentSourceWithSlotsInterface; -use Drupal\experience_builder\Entity\ComponentTreeEntityInterface; +use Drupal\experience_builder\Entity\ContentTemplate; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -31,7 +31,7 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { } $root = $this->context->getRoot()->getEntity(); - assert($root instanceof ComponentTreeEntityInterface); + assert($root instanceof ContentTemplate); /** @var \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure $tree */ $tree = $root->getComponentTree()->get('tree'); @@ -68,6 +68,12 @@ final class ValidExposedSlotConstraintValidator extends ConstraintValidator { break; } } + + if ($root->getMode() !== $constraint->viewMode) { + $this->context->addViolation($constraint->viewModeMismatchMessage, [ + '%mode' => $constraint->viewMode, + ]); + } } } diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 7ff49f8303..17b6db9099 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -459,4 +459,37 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ]); } + public function testExposedSlotsOnlyAllowedInFullViewMode(): void { + $this->entity = ContentTemplate::create([ + 'content_entity_type_id' => 'node', + 'content_entity_type_bundle' => 'alpha', + 'content_entity_type_view_mode' => 'teaser', + 'component_tree' => [ + 'tree' => self::encodeXBData([ + ComponentTreeStructure::ROOT_UUID => [ + ['uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', 'component' => 'sdc.xb_test_sdc.props-slots'], + ], + ]), + 'inputs' => self::encodeXBData([ + 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef' => [ + 'heading' => [ + 'sourceType' => 'dynamic', + 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', + ], + ], + ]), + ], + 'exposed_slots' => [ + 'footer_for_you' => [ + 'component_uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', + 'slot_name' => 'the_footer', + 'label' => "I got your footer right here", + ], + ], + ]); + $this->assertValidationErrors([ + 'exposed_slots.footer_for_you' => 'Exposed slots are only allowed in the <em class="placeholder">full</em> view mode.', + ]); + } + } -- GitLab From 798b0cc6c7409c28949de336f9349b18327d5bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 15 Apr 2025 15:19:27 -0400 Subject: [PATCH 09/17] Fix a small PHPStan complaint --- src/Entity/ContentTemplate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/ContentTemplate.php b/src/Entity/ContentTemplate.php index 807890e4bc..5bce583beb 100644 --- a/src/Entity/ContentTemplate.php +++ b/src/Entity/ContentTemplate.php @@ -102,7 +102,7 @@ final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEnt /** * The exposed slots. * - * @var ?array<string, array{'component_uuid': string, 'slot_name': string, 'label': string}> + * @var array<string, array{'component_uuid': string, 'slot_name': string, 'label': string}> */ protected array $exposed_slots = []; -- GitLab From 5c8149040ea9cff7474943774a0fba713fcd8cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 15 Apr 2025 15:40:20 -0400 Subject: [PATCH 10/17] Fix test failures --- src/Entity/ContentTemplate.php | 4 ++-- .../Kernel/Config/ContentTemplateValidationTest.php | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Entity/ContentTemplate.php b/src/Entity/ContentTemplate.php index 5bce583beb..b3fcdd0fde 100644 --- a/src/Entity/ContentTemplate.php +++ b/src/Entity/ContentTemplate.php @@ -102,9 +102,9 @@ final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEnt /** * The exposed slots. * - * @var array<string, array{'component_uuid': string, 'slot_name': string, 'label': string}> + * @var ?array<string, array{'component_uuid': string, 'slot_name': string, 'label': string}> */ - protected array $exposed_slots = []; + protected ?array $exposed_slots = []; /** * Tries to load a template for a particular entity, in a specific view mode. diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 17b6db9099..078c96afaa 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -29,11 +29,6 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe */ protected bool $hasLabel = FALSE; - /** - * {@inheritdoc} - */ - protected static array $propertiesWithOptionalValues = ['exposed_slots']; - /** * {@inheritdoc} */ @@ -371,10 +366,7 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ], ]); $this->assertValidationErrors([ - 'exposed_slots.not_allowed' => [ - 'Exposing the full component tree is not allowed.', - 'The component <em class="placeholder">a548b48d-58a8-4077-aa04-da9405a6f418</em> does not exist in the tree.', - ], + 'exposed_slots.not_allowed' => 'Exposing the full component tree is not allowed.', ]); } -- GitLab From 85ec3000085dc84cf42444d9d82c7042647a1a3e Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Wed, 16 Apr 2025 14:15:10 +0000 Subject: [PATCH 11/17] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Wim Leers <44946-wimleers@users.noreply.drupalcode.org> --- config/schema/experience_builder.schema.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index d81df6ecef..c27928f90c 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -583,8 +583,8 @@ experience_builder.content_template.*.*.*: # The UUID of the component that contains the slot being exposed. This CANNOT # be the same as \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure::ROOT_UUID, # because that would mean the entire template could be overridden by an individual - # content entity, which is not an appropriate use of a content template (we have - # a whole distinct landing page content entity type for that). + # content entity, which is not an appropriate use of a content template (XB has + # the `Page` content entity type for that). component_uuid: type: uuid label: 'UUID of the component instance that contains the exposed slot' -- GitLab From 25d9d0543a6f7ffc12b0748b5bc0401b429a3aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Wed, 16 Apr 2025 10:58:42 -0400 Subject: [PATCH 12/17] Move todo --- config/schema/experience_builder.schema.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index c27928f90c..6ffcd962c3 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -524,6 +524,9 @@ experience_builder.content_template.*.*.*: constraints: # @todo Use `ConfigExists` once https://www.drupal.org/project/drupal/issues/3518273 is # fixed in core. + # @todo Core entity types ship with a `full` view mode, but it's not guaranteed to + # exist for every content entity type. Ensure XB creates it when opting a content + # entity bundle into XB rendering in https://www.drupal.org/i/3518248. BetterConfigExists: prefix: 'core.entity_view_mode.[%parent.content_entity_type_id].' component_tree: @@ -561,7 +564,8 @@ experience_builder.content_template.*.*.*: type: mapping constraints: # Exposed slots are only supported by the "canonical" view mode, which is generally - # `full`. Example: + # assumed to be `full` (see \Drupal\Core\Entity\Controller\EntityViewController::view() + # for where core makes this assumption). Example: # # exposed_slots: # profile_bio: @@ -572,12 +576,6 @@ experience_builder.content_template.*.*.*: # label: 'Intro' # component_uuid: 98bcab26-e434-4ad4-9eaf-0520bdb32fcc # slot_name: body - # - # @see \Drupal\Core\Entity\Controller\EntityViewController::view() - # - # @todo Core entity types ship with a `full` view mode, but it's not guaranteed to - # exist for every content entity type. Ensure XB creates it when opting a content - # entity bundle into XB rendering in https://www.drupal.org/i/3518248. ValidExposedSlot: full mapping: # The UUID of the component that contains the slot being exposed. This CANNOT -- GitLab From 75dcc7745ac4378d62e6bf31ef7d1c3eb03443f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Wed, 16 Apr 2025 11:27:47 -0400 Subject: [PATCH 13/17] Refactor the test partially --- src/Entity/ContentTemplate.php | 2 +- .../Config/ContentTemplateValidationTest.php | 197 ++++++++---------- 2 files changed, 90 insertions(+), 109 deletions(-) diff --git a/src/Entity/ContentTemplate.php b/src/Entity/ContentTemplate.php index b3fcdd0fde..f3b6bf85ef 100644 --- a/src/Entity/ContentTemplate.php +++ b/src/Entity/ContentTemplate.php @@ -69,7 +69,7 @@ final class ContentTemplate extends ConfigEntityBase implements ComponentTreeEnt * * @see \Drupal\experience_builder\Plugin\Validation\Constraint\StringPartsConstraint */ - protected string $id; + protected ?string $id; /** * Entity type to be displayed. diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 078c96afaa..be3b27d623 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -87,42 +87,51 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'tree' => self::encodeXBData([ ComponentTreeStructure::ROOT_UUID => [ // An SDC populated by static prop sources. - ['uuid' => 'sdc-static', 'component' => 'sdc.sdc_test.my-cta'], + ['uuid' => '4d537f4f-d2dc-487f-92a2-7143babb0ea3', 'component' => 'sdc.sdc_test.my-cta'], // A code component populated by an entity base field. - ['uuid' => 'code-dynamic-base-field', 'component' => 'js.my-cta'], + ['uuid' => 'ef1e9d37-8d76-476e-b39f-37b4dd5a4767', 'component' => 'js.my-cta'], // An SDC populated by a normal entity field. - ['uuid' => 'sdc-dynamic-bundle-field', 'component' => 'sdc.xb_test_sdc.props-no-slots'], + ['uuid' => '3b8b5d79-e133-4360-a064-7a89aab99386', 'component' => 'sdc.xb_test_sdc.props-no-slots'], // A block component. - ['uuid' => 'block', 'component' => 'block.system_branding_block'], + ['uuid' => '2f4c5088-c5b9-4d1c-ac57-79f1f3c78dee', 'component' => 'block.system_branding_block'], + // An SDC with a slot that can be exposed. + ['uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', 'component' => 'sdc.xb_test_sdc.props-slots'], ], ]), 'inputs' => self::encodeXBData([ - 'sdc-static' => [ + '4d537f4f-d2dc-487f-92a2-7143babb0ea3' => [ 'text' => [ 'sourceType' => 'static:field_item:string', 'value' => 'This is really tricky for a first-timer', 'expression' => 'ℹ︎string␟value', ], ], - 'code-dynamic-base-field' => [ + 'ef1e9d37-8d76-476e-b39f-37b4dd5a4767' => [ 'text' => [ 'sourceType' => 'dynamic', 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', ], ], - 'sdc-dynamic-bundle-field' => [ + '3b8b5d79-e133-4360-a064-7a89aab99386' => [ 'heading' => [ 'sourceType' => 'dynamic', 'expression' => 'ℹ︎␜entity:node:alpha␝field_test␞␟value', ], ], - 'block' => [ + '2f4c5088-c5b9-4d1c-ac57-79f1f3c78dee' => [ 'label' => '', 'label_display' => FALSE, 'use_site_logo' => FALSE, 'use_site_name' => TRUE, 'use_site_slogan' => TRUE, ], + 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef' => [ + 'heading' => [ + 'sourceType' => 'static:field_item:string', + 'value' => 'There be a slot here', + 'expression' => 'ℹ︎string␟value', + ], + ], ]), ], ]); @@ -148,6 +157,7 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'experience_builder.component.js.my-cta', 'experience_builder.component.sdc.sdc_test.my-cta', 'experience_builder.component.sdc.xb_test_sdc.props-no-slots', + 'experience_builder.component.sdc.xb_test_sdc.props-slots', ], ], $this->entity->getDependencies() @@ -159,6 +169,7 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'experience_builder.component.js.my-cta', 'experience_builder.component.sdc.sdc_test.my-cta', 'experience_builder.component.sdc.xb_test_sdc.props-no-slots', + 'experience_builder.component.sdc.xb_test_sdc.props-slots', 'experience_builder.js_component.my-cta', ], 'module' => [ @@ -357,63 +368,29 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ]); } - public function testRootUuidCannotBeExposed(): void { - $this->entity->set('exposed_slots', [ - 'not_allowed' => [ - 'component_uuid' => ComponentTreeStructure::ROOT_UUID, - 'slot_name' => 'not-a-thing', - 'label' => "This won't work", - ], - ]); - $this->assertValidationErrors([ - 'exposed_slots.not_allowed' => 'Exposing the full component tree is not allowed.', - ]); - } - - public function testExposedSlotComponentMustExistInTree(): void { - $this->entity->set('exposed_slots', [ - 'not_a_thing' => [ - 'component_uuid' => 'f0a5a653-ee1c-4469-903a-119d70edbf02', - 'slot_name' => 'not-a-thing', - 'label' => "Can't expose a slot in a component we don't have!", - ], - ]); - $this->assertValidationErrors([ - 'exposed_slots.not_a_thing' => 'The component <em class="placeholder">f0a5a653-ee1c-4469-903a-119d70edbf02</em> does not exist in the tree.', - ]); - } - public function testExposedSlotMustBeEmpty(): void { + assert($this->entity instanceof ContentTemplate); + + // Add a component in one of the open slots. + $item = $this->entity->getComponentTree(); + $tree = json_decode($item->get('tree')->getValue(), TRUE); + $tree['b4937e35-ddc2-4f36-8d4c-b1cc14aaefef']['the_footer'][] = [ + 'uuid' => 'greeting', + 'component' => 'sdc.xb_test_sdc.props-no-slots', + ]; + $inputs = json_decode($item->get('inputs')->getValue(), TRUE); + $inputs['greeting']['heading'] = [ + 'sourceType' => 'dynamic', + 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', + ]; $this->entity->set('component_tree', [ - 'tree' => self::encodeXBData([ - ComponentTreeStructure::ROOT_UUID => [ - ['uuid' => '67062b9a-b35f-453f-a66b-fdb9988b604b', 'component' => 'sdc.xb_test_sdc.props-slots'], - ], - '67062b9a-b35f-453f-a66b-fdb9988b604b' => [ - 'the_footer' => [ - ['uuid' => 'greeting', 'component' => 'sdc.xb_test_sdc.props-no-slots'], - ], - ], - ]), - 'inputs' => self::encodeXBData([ - '67062b9a-b35f-453f-a66b-fdb9988b604b' => [ - 'heading' => [ - 'sourceType' => 'static:field_item:string', - 'value' => "My footer is showing", - 'expression' => 'ℹ︎string␟value', - ], - ], - 'greeting' => [ - 'heading' => [ - 'sourceType' => 'dynamic', - 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', - ], - ], - ]), + 'tree' => self::encodeXBData($tree), + 'inputs' => self::encodeXBData($inputs), ]); + $this->entity->set('exposed_slots', [ 'filled_footer' => [ - 'component_uuid' => '67062b9a-b35f-453f-a66b-fdb9988b604b', + 'component_uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', 'slot_name' => 'the_footer', 'label' => "Something's already here!", ], @@ -423,61 +400,65 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe ]); } - public function testExposedSlotMustBeDefinedByComponent(): void { - $this->entity->set('component_tree', [ - 'tree' => self::encodeXBData([ - ComponentTreeStructure::ROOT_UUID => [ - ['uuid' => '20398504-ce6f-4a79-819e-405ea34eefc4', 'component' => 'sdc.xb_test_sdc.props-slots'], - ], - ]), - 'inputs' => self::encodeXBData([ - '20398504-ce6f-4a79-819e-405ea34eefc4' => [ - 'heading' => [ - 'sourceType' => 'dynamic', - 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', - ], + public static function providerInvalidExposedSlot(): iterable { + yield 'root uuid is exposed' => [ + [ + 'not_allowed' => [ + 'component_uuid' => ComponentTreeStructure::ROOT_UUID, + 'slot_name' => 'not-a-thing', + 'label' => "This won't work", ], - ]), - ]); - $this->entity->set('exposed_slots', [ - 'filled_footer' => [ - 'component_uuid' => '20398504-ce6f-4a79-819e-405ea34eefc4', - 'slot_name' => 'not_a_real_slot', - 'label' => "Whither this slot you speak of?", ], - ]); - $this->assertValidationErrors([ - 'exposed_slots.filled_footer' => 'The component <em class="placeholder">20398504-ce6f-4a79-819e-405ea34eefc4</em> does not have a <em class="placeholder">not_a_real_slot</em> slot.', - ]); - } + [ + 'exposed_slots.not_allowed' => 'Exposing the full component tree is not allowed.', + ], + ]; - public function testExposedSlotsOnlyAllowedInFullViewMode(): void { - $this->entity = ContentTemplate::create([ - 'content_entity_type_id' => 'node', - 'content_entity_type_bundle' => 'alpha', - 'content_entity_type_view_mode' => 'teaser', - 'component_tree' => [ - 'tree' => self::encodeXBData([ - ComponentTreeStructure::ROOT_UUID => [ - ['uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', 'component' => 'sdc.xb_test_sdc.props-slots'], - ], - ]), - 'inputs' => self::encodeXBData([ - 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef' => [ - 'heading' => [ - 'sourceType' => 'dynamic', - 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', - ], - ], - ]), + yield 'component exposing the slot does not exist in the tree' => [ + [ + 'not_a_thing' => [ + 'component_uuid' => '6348ee20-cf62-49e3-bc86-cf62abc09c74', + 'slot_name' => 'not-a-thing', + 'label' => "Can't expose a slot in a component we don't have!", + ], ], - 'exposed_slots' => [ - 'footer_for_you' => [ + [ + 'exposed_slots.not_a_thing' => 'The component <em class="placeholder">6348ee20-cf62-49e3-bc86-cf62abc09c74</em> does not exist in the tree.', + ], + ]; + + yield 'exposed slot is not defined by the component' => [ + [ + 'filled_footer' => [ 'component_uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', - 'slot_name' => 'the_footer', - 'label' => "I got your footer right here", + 'slot_name' => 'not_a_real_slot', + 'label' => "Whither this slot you speak of?", ], ], + [ + 'exposed_slots.filled_footer' => 'The component <em class="placeholder">b4937e35-ddc2-4f36-8d4c-b1cc14aaefef</em> does not have a <em class="placeholder">not_a_real_slot</em> slot.', + ], + ]; + } + + /** + * @dataProvider providerInvalidExposedSlot + */ + public function testInvalidExposedSlot(array $exposed_slots, array $expected_errors): void { + $this->entity->set('exposed_slots', $exposed_slots); + $this->assertValidationErrors($expected_errors); + } + + public function testExposedSlotsOnlyAllowedInFullViewMode(): void { + $this->entity = $this->entity->createDuplicate(); + $this->entity->set('content_entity_type_view_mode', 'teaser'); + $this->entity->set('id', 'node.alpha.teaser'); + $this->entity->set('exposed_slots', [ + 'footer_for_you' => [ + 'component_uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', + 'slot_name' => 'the_footer', + 'label' => "I got your footer right here", + ], ]); $this->assertValidationErrors([ 'exposed_slots.footer_for_you' => 'Exposed slots are only allowed in the <em class="placeholder">full</em> view mode.', -- GitLab From 06a54825ec25ca2985c19b17e21faa4d82dc086b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Wed, 16 Apr 2025 11:59:25 -0400 Subject: [PATCH 14/17] Validate slot machine names --- config/schema/experience_builder.schema.yml | 4 +++ .../SequenceKeysMatchRegexConstraint.php | 19 +++++++++++ ...uenceKeysMatchRegexConstraintValidator.php | 33 +++++++++++++++++++ .../Config/ContentTemplateValidationTest.php | 13 ++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraint.php create mode 100644 src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index 6ffcd962c3..f0550642f6 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -592,3 +592,7 @@ experience_builder.content_template.*.*.*: label: type: required_label label: 'A human-readable label for the exposed slot' + constraints: + SequenceKeysMatchRegex: + pattern: '/^[a-z0-9_]+$/' + message: "The %value key is not a valid machine name." diff --git a/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraint.php b/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraint.php new file mode 100644 index 0000000000..8004db2ee0 --- /dev/null +++ b/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraint.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\experience_builder\Plugin\Validation\Constraint; + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Validation\Attribute\Constraint; +use Symfony\Component\Validator\Constraints\Regex; + +#[Constraint( + id: self::PLUGIN_ID, + label: new TranslatableMarkup('Sequence keys match regex', options: ['context' => 'Validation']), +)] +final class SequenceKeysMatchRegexConstraint extends Regex { + + public const string PLUGIN_ID = 'SequenceKeysMatchRegex'; + +} diff --git a/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php b/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php new file mode 100644 index 0000000000..35044e5dd2 --- /dev/null +++ b/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\experience_builder\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\RegexValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +/** + * Validates the SequenceKeysMatchRegex constraint. + */ +final class SequenceKeysMatchRegexConstraintValidator extends RegexValidator { + + /** + * {@inheritdoc} + */ + public function validate(mixed $value, Constraint $constraint): void { + if (!is_array($value)) { + throw new UnexpectedTypeException($value, 'array'); + } + + if ($value && array_is_list($value)) { + throw new UnexpectedTypeException($value, 'associative array'); + } + + foreach (array_keys($value) as $key) { + parent::validate($key, $constraint); + } + } + +} diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index be3b27d623..37b14663d8 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -439,6 +439,19 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'exposed_slots.filled_footer' => 'The component <em class="placeholder">b4937e35-ddc2-4f36-8d4c-b1cc14aaefef</em> does not have a <em class="placeholder">not_a_real_slot</em> slot.', ], ]; + + yield 'exposed slot machine name is not valid' => [ + [ + 'not a valid exposed slot name' => [ + 'component_uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', + 'slot_name' => 'the_footer', + 'label' => "I got your footer right here", + ], + ], + [ + 'exposed_slots' => 'The <em class="placeholder">"not a valid exposed slot name"</em> key is not a valid machine name.', + ], + ]; } /** -- GitLab From c7b8b71589be4cc58fc238841d0e4f00f90d2a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Wed, 16 Apr 2025 14:02:45 -0400 Subject: [PATCH 15/17] Fix test --- .../Constraint/SequenceKeysMatchRegexConstraintValidator.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php b/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php index 35044e5dd2..07f4b17379 100644 --- a/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/SequenceKeysMatchRegexConstraintValidator.php @@ -17,10 +17,13 @@ final class SequenceKeysMatchRegexConstraintValidator extends RegexValidator { * {@inheritdoc} */ public function validate(mixed $value, Constraint $constraint): void { + // If the value isn't NULL, it needs to be an associative array. + if ($value === NULL) { + return; + } if (!is_array($value)) { throw new UnexpectedTypeException($value, 'array'); } - if ($value && array_is_list($value)) { throw new UnexpectedTypeException($value, 'associative array'); } -- GitLab From 8796a48077e970afbfe0a4d932a5b180f2a3b2e1 Mon Sep 17 00:00:00 2001 From: Wim Leers <44946-wimleers@users.noreply.drupalcode.org> Date: Fri, 18 Apr 2025 10:12:18 +0000 Subject: [PATCH 16/17] =?UTF-8?q?Clarifications=20WRT=20"exposed=20slot=20?= =?UTF-8?q?name"=20vs=20"component=20slot=20name"=C2=A0=E2=80=94=20they=20?= =?UTF-8?q?are=20NOT=20the=20same=20thing!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/schema/experience_builder.schema.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index f0550642f6..19722e2bc5 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -587,12 +587,14 @@ experience_builder.content_template.*.*.*: type: uuid label: 'UUID of the component instance that contains the exposed slot' slot_name: + # @todo Add constraint in https://www.drupal.org/project/experience_builder/issues/3519891 type: string - label: 'The name of the slot exposed slot, as known to the component' + label: 'The machine name of the component slot being exposed' label: type: required_label label: 'A human-readable label for the exposed slot' constraints: SequenceKeysMatchRegex: + # Note: this is a subset of what slot names are allowed by SDCs: the machine names of XB's exposed content template slots must be simpler. pattern: '/^[a-z0-9_]+$/' message: "The %value key is not a valid machine name." -- GitLab From c5b8f77a19049ade8f502c13bc73e3505c8c63d8 Mon Sep 17 00:00:00 2001 From: Wim Leers <wim.leers@acquia.com> Date: Fri, 18 Apr 2025 12:20:45 +0200 Subject: [PATCH 17/17] Revert bits of 4c246eae to keep the test equally easy to read and reduce the diff compared to HEAD. --- .../Config/ContentTemplateValidationTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/src/Kernel/Config/ContentTemplateValidationTest.php b/tests/src/Kernel/Config/ContentTemplateValidationTest.php index 37b14663d8..f754e97841 100644 --- a/tests/src/Kernel/Config/ContentTemplateValidationTest.php +++ b/tests/src/Kernel/Config/ContentTemplateValidationTest.php @@ -87,38 +87,38 @@ final class ContentTemplateValidationTest extends BetterConfigEntityValidationTe 'tree' => self::encodeXBData([ ComponentTreeStructure::ROOT_UUID => [ // An SDC populated by static prop sources. - ['uuid' => '4d537f4f-d2dc-487f-92a2-7143babb0ea3', 'component' => 'sdc.sdc_test.my-cta'], + ['uuid' => 'sdc-static', 'component' => 'sdc.sdc_test.my-cta'], // A code component populated by an entity base field. - ['uuid' => 'ef1e9d37-8d76-476e-b39f-37b4dd5a4767', 'component' => 'js.my-cta'], + ['uuid' => 'code-dynamic-base-field', 'component' => 'js.my-cta'], // An SDC populated by a normal entity field. - ['uuid' => '3b8b5d79-e133-4360-a064-7a89aab99386', 'component' => 'sdc.xb_test_sdc.props-no-slots'], + ['uuid' => 'sdc-dynamic-bundle-field', 'component' => 'sdc.xb_test_sdc.props-no-slots'], // A block component. - ['uuid' => '2f4c5088-c5b9-4d1c-ac57-79f1f3c78dee', 'component' => 'block.system_branding_block'], + ['uuid' => 'block', 'component' => 'block.system_branding_block'], // An SDC with a slot that can be exposed. ['uuid' => 'b4937e35-ddc2-4f36-8d4c-b1cc14aaefef', 'component' => 'sdc.xb_test_sdc.props-slots'], ], ]), 'inputs' => self::encodeXBData([ - '4d537f4f-d2dc-487f-92a2-7143babb0ea3' => [ + 'sdc-static' => [ 'text' => [ 'sourceType' => 'static:field_item:string', 'value' => 'This is really tricky for a first-timer', 'expression' => 'ℹ︎string␟value', ], ], - 'ef1e9d37-8d76-476e-b39f-37b4dd5a4767' => [ + 'code-dynamic-base-field' => [ 'text' => [ 'sourceType' => 'dynamic', 'expression' => 'ℹ︎␜entity:node:alpha␝title␞␟value', ], ], - '3b8b5d79-e133-4360-a064-7a89aab99386' => [ + 'sdc-dynamic-bundle-field' => [ 'heading' => [ 'sourceType' => 'dynamic', 'expression' => 'ℹ︎␜entity:node:alpha␝field_test␞␟value', ], ], - '2f4c5088-c5b9-4d1c-ac57-79f1f3c78dee' => [ + 'block' => [ 'label' => '', 'label_display' => FALSE, 'use_site_logo' => FALSE, -- GitLab