diff --git a/config/schema/experience_builder.schema.yml b/config/schema/experience_builder.schema.yml index b3c76f7716f54998b880fc386e508372f6288841..e3352d5bc99ae19fcc9ebf7e126ccfa844f5ab38 100644 --- a/config/schema/experience_builder.schema.yml +++ b/config/schema/experience_builder.schema.yml @@ -28,6 +28,15 @@ experience_builder.component.*: Length: # @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH max: 166 + provider: + type: string + label: 'Name of the module or theme providing this component, or null if provided via something else' + nullable: true + constraints: + NotBlank: + allowNull: true + ExtensionName: [] + Callback: ['\Drupal\experience_builder\Entity\Component', providerExists] source: type: string label: 'Source plugin' diff --git a/docs/components.md b/docs/components.md index c90582957d78c4ee094deef0480f79653a5c1d5c..6744246213f3860b8df8a03157675b906f880db3 100644 --- a/docs/components.md +++ b/docs/components.md @@ -93,7 +93,12 @@ Therefore, it only makes sense to surface _block plugins_ as XB `component`s. defined using config schema (`type: block.settings.<PLUGIN ID>`). Defaults are present as the `::defaultConfiguration()` method on the PHP plugin class. -`Block` `component`s specify the accepted explicit inputs +`Block` `component`s DO accept implicit inputs, in two ways even: +1. Logic in the block plugin can fetch data — through database queries, HTTP requests, anything. +2. Contexts. Not yet supported. (⚠️ handling contexts is still TBD in [#3485502](https://www.drupal.org/project/experience_builder/issues/3485502)) + +`Block` `component`s specify the accepted explicit inputs. They typically allow influencing the logic in the block +plugin. These explicit inputs can hence be seen as knobs and levers to adjust what the underlying block plugin does. `Block` DOES provide an input UX (`BlockPluginInterface::buildConfigurationForm()`), so its `Component Source Plugin` simply reuses that. @@ -128,6 +133,8 @@ refer to "blocks" and not "block plugins", under the hood, they actually _are_ b 3.1.1 above](#3.1.1)). See [section 3.2 `JavaScriptComponent config entity` in the `XB Config Management` doc](config-management.md#3.2) for all details. +`JS` `component`s DO NOT accept implicit inputs. + `JS` DOES NOT provide an input UX, so its `Component Source Plugin` must do so on its behalf; and does so by matching available field types against the JSON schema of its explicit inputs ("props"). For details, see the [`XB Shape Matching into Field Types` doc](shape-matching-into-field-types.md). (It shares this infrastructure with the `SDC` `Component diff --git a/experience_builder.module b/experience_builder.module index ca9833a8d13d74cfa7419bbe51f5833c9f7d890c..4e5efc170f4c6d32dd80164f0bf75966d304c062 100644 --- a/experience_builder.module +++ b/experience_builder.module @@ -265,6 +265,7 @@ function experience_builder_block_alter(array &$definitions): void { 'label' => (string) new TranslatableMarkup('@label block', ['@label' => $definition['admin_label']]), 'category' => (string) $definition['category'], 'source' => BlockComponent::SOURCE_PLUGIN_ID, + 'provider' => $definition['provider'], 'settings' => [ 'plugin_id' => $id, // We are using strict config schema validation, so we need to provide valid default settings for each block. diff --git a/openapi.yml b/openapi.yml index 2aa4d5d3b17970056a36c643f8a40feb6ac3cdca..7df1084fd395b4ccc263ab19bd345f7fbb448db4 100644 --- a/openapi.yml +++ b/openapi.yml @@ -164,6 +164,7 @@ paths: name: Call to Action source: Module component category: Buttons + library: extension_components transforms: text: extractMainPropertyName: @@ -1089,6 +1090,7 @@ components: - id - category - default_markup + - library properties: name: type: string @@ -1099,6 +1101,18 @@ components: category: type: string description: The component category + library: + type: string + description: Library ID + enum: + # Provided by Experience Builder + - elements + # Provided by Modules + - extension_components + # Blocks + - dynamic_components + # SDCs from the Active Theme + JavaScript Code Components + - primary_components transforms: type: object description: Transform configuration diff --git a/src/Attribute/ComponentSource.php b/src/Attribute/ComponentSource.php index f714ac961193055ac6184954fa449f8f793ec8cb..0c3e17600030f0b3f0ecfbad34eb7a483e097d02 100644 --- a/src/Attribute/ComponentSource.php +++ b/src/Attribute/ComponentSource.php @@ -25,6 +25,7 @@ final class ComponentSource extends Plugin { public function __construct( public readonly string $id, public readonly TranslatableMarkup $label, + public readonly bool $supportsImplicitInputs, public readonly ?string $deriver = NULL, ) { } diff --git a/src/Controller/ApiConfigControllers.php b/src/Controller/ApiConfigControllers.php index 93b303a43fe82bfdb6470cc7ac9c072d72325387..62d02fc2ceee52d74684e5a4a48ea4b85805c940 100644 --- a/src/Controller/ApiConfigControllers.php +++ b/src/Controller/ApiConfigControllers.php @@ -84,12 +84,13 @@ final class ApiConfigControllers extends ApiControllerBase { if (!$xb_config_entity_type->get('xb_visible_when_disabled')) { $query->condition('status', TRUE); } - /** @var array<\Drupal\experience_builder\Entity\XbHttpApiEligibleConfigEntityInterface> $config_entities */ - $config_entities = $storage->loadMultiple($query->execute()); $query_cacheability = (new CacheableMetadata()) ->addCacheContexts($xb_config_entity_type->getListCacheContexts()) ->addCacheTags($xb_config_entity_type->getListCacheTags()); + $xb_config_entity_type->getClass()::refineListQuery($query, $query_cacheability); + /** @var array<\Drupal\experience_builder\Entity\XbHttpApiEligibleConfigEntityInterface> $config_entities */ + $config_entities = $storage->loadMultiple($query->execute()); $normalizations = []; $normalizations_cacheability = new CacheableMetadata(); diff --git a/src/Entity/AssetLibrary.php b/src/Entity/AssetLibrary.php index 7cf7104c314580d63acefcebf71d45d15d61933e..727266ced65be4dfa0c6abb357a4a834dc6b3497 100644 --- a/src/Entity/AssetLibrary.php +++ b/src/Entity/AssetLibrary.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Drupal\experience_builder\Entity; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\Entity\Query\QueryInterface; use Drupal\experience_builder\ClientSideRepresentation; /** @@ -73,4 +75,11 @@ final class AssetLibrary extends ConfigEntityBase implements XbHttpApiEligibleCo return $data; } + /** + * {@inheritdoc} + */ + public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void { + // Nothing to do. + } + } diff --git a/src/Entity/Component.php b/src/Entity/Component.php index 2ab3d8bced00b10d69fe5a6be3391bf3bf5160cb..62fcf3facf16091bd507bf26926c2b13880e2aa1 100644 --- a/src/Entity/Component.php +++ b/src/Entity/Component.php @@ -4,7 +4,11 @@ declare(strict_types=1); namespace Drupal\experience_builder\Entity; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection; use Drupal\Core\Render\Markup; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -46,6 +50,7 @@ use Drupal\experience_builder\ComponentSource\ComponentSourceManager; * "label", * "id", * "source", + * "provider", * "category", * "settings", * }, @@ -75,6 +80,16 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb */ protected string $source; + /** + * The provider of this component: a valid module or theme name, or NULL. + * + * NULL must be used to signal it's not provided by an extension. This is used + * for "code components" for example — which are provided by entities. + * + * @see \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent + */ + protected ?string $provider; + /** * The human-readable category of the component. */ @@ -168,10 +183,17 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb } /** - * {@inheritdoc} + * Works around the `ExtensionExists` constraint requiring a fixed type. + * + * @see \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionExistsConstraintValidator + * @see https://www.drupal.org/node/3353397 */ - protected function providerExists(string $provider): bool { - return $this->moduleHandler()->moduleExists($provider) || $this->themeHandler()->themeExists($provider); + public static function providerExists(?string $provider): bool { + if (is_null($provider)) { + return TRUE; + } + $container = \Drupal::getContainer(); + return $container->get(ModuleHandlerInterface::class)->moduleExists($provider) || $container->get(ThemeHandlerInterface::class)->themeExists($provider); } /** @@ -206,18 +228,11 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb $component_config_entity_uuid = $this->uuid(); $build['#prefix'] = Markup::create("<!-- xb-start-$component_config_entity_uuid -->"); $build['#suffix'] = Markup::create("<!-- xb-end-$component_config_entity_uuid -->"); - - $info += [ - 'id' => $this->id(), - 'name' => (string) $this->label(), - 'category' => (string) $this->getCategory(), - 'source' => (string) $this->getComponentSource()->getPluginDefinition()['label'], - ]; - return ClientSideRepresentation::create( values: $info + [ 'id' => $this->id(), 'name' => (string) $this->label(), + 'library' => $this->computeUiLibrary()->value, 'category' => (string) $this->getCategory(), 'source' => (string) $this->getComponentSource()->getPluginDefinition()['label'], ], @@ -225,6 +240,50 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb )->addCacheableDependency($this); } + /** + * Uses heuristics to compute the appropriate "library" in the XB UI. + * + * Each Component appears in a well-defined "library" in the XB UI. This is a + * set of heuristics with a particular decision tree. + * + * @see https://www.drupal.org/project/experience_builder/issues/3498419#comment-15997505 + */ + private function computeUiLibrary(): LibraryEnum { + $config = \Drupal::configFactory()->loadMultiple(['core.extension', 'system.theme']); + $installed_modules = [ + 'core', + ...array_keys($config['core.extension']->get('module')), + ]; + // @see \Drupal\Core\Extension\ThemeHandler::getDefault() + $default_theme = $config['system.theme']->get('default'); + + // 1. Is the component dynamic (consumes implicit inputs/context or has + // logic)? + if ($this->getComponentSource()->getPluginDefinition()['supportsImplicitInputs']) { + return LibraryEnum::DynamicComponents; + } + + // 2. Is the component provided by a module? + if (in_array($this->provider, $installed_modules, TRUE)) { + return $this->provider === 'experience_builder' + // 2.B Is the providing module XB? + ? LibraryEnum::Elements + : LibraryEnum::ExtensionComponents; + } + + // 3. Is the component provided by the default theme (or its base theme)? + if ($this->provider === $default_theme) { + return LibraryEnum::PrimaryComponents; + } + + // 4. Is the component provided by neither a theme nor a module? + if ($this->provider === NULL) { + return LibraryEnum::PrimaryComponents; + } + + throw new \LogicException('A Component is being normalized that belongs in no XB UI library.'); + } + /** * {@inheritdoc} * @@ -234,6 +293,34 @@ final class Component extends ConfigEntityBase implements ComponentInterface, Xb throw new \LogicException('Not supported: read-only for the client side, mutable only on the server side.'); } + /** + * {@inheritdoc} + */ + public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void { + $container = \Drupal::getContainer(); + $theme_handler = $container->get(ThemeHandlerInterface::class); + $installed_themes = array_keys($theme_handler->listInfo()); + $default_theme = $theme_handler->getDefault(); + + // Omit Components provided by installed-but-not-default themes. This keeps + // all other Components: + // - module-provided ones + // - default theme-provided + // - provided by something else than an extension, such as an entity. + $or_group = $query->orConditionGroup() + ->condition('provider', operator: 'NOT IN', value: array_diff($installed_themes, [$default_theme])) + ->condition('provider', operator: 'IS NULL'); + $query->condition($or_group); + + // Reflect the conditions added to the query in the cacheability. + $cacheability->addCacheTags([ + // The set of installed themes is stored in the `core.extension` config. + 'config:core.extension', + // The default theme is stored in the `system.theme` config. + 'config:system.theme', + ]); + } + /** * {@inheritdoc} */ diff --git a/src/Entity/JavaScriptComponent.php b/src/Entity/JavaScriptComponent.php index b23e6ad96d874272119c12222f99e5f8bedce612..898c6135a8f6dd7b9a56e6c7e6b93c08f0956197 100644 --- a/src/Entity/JavaScriptComponent.php +++ b/src/Entity/JavaScriptComponent.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Drupal\experience_builder\Entity; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\Entity\Query\QueryInterface; use Drupal\experience_builder\ClientSideRepresentation; /** @@ -139,6 +141,13 @@ final class JavaScriptComponent extends ConfigEntityBase implements XbHttpApiEli ]; } + /** + * {@inheritdoc} + */ + public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void { + // Nothing to do. + } + /** * Code components are not Twig-defined but still aim to match SDC closely. * diff --git a/src/Entity/LibraryEnum.php b/src/Entity/LibraryEnum.php new file mode 100644 index 0000000000000000000000000000000000000000..2d05f9ce4dcea2dc9301d4e43de5d2fb4315d7e8 --- /dev/null +++ b/src/Entity/LibraryEnum.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\experience_builder\Entity; + +/** + * @internal + * @see \Drupal\experience_builder\Entity\Component::computeUiLibrary() + */ +enum LibraryEnum: string { + case Elements = 'elements'; + case ExtensionComponents = 'extension_components'; + case DynamicComponents = 'dynamic_components'; + case PrimaryComponents = 'primary_components'; +} diff --git a/src/Entity/Pattern.php b/src/Entity/Pattern.php index ddb23dad5f7cce3e90ff6264b640a4f10494c2ea..c5afbd8d1d2c16d7bb3bdbdf6e306014aa6b4093 100644 --- a/src/Entity/Pattern.php +++ b/src/Entity/Pattern.php @@ -5,8 +5,10 @@ declare(strict_types=1); namespace Drupal\experience_builder\Entity; use Drupal\Component\Utility\Random; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\Query\QueryInterface; use Drupal\experience_builder\ClientSideRepresentation; use Drupal\experience_builder\Controller\ApiConfigControllers; use Drupal\experience_builder\Controller\ClientServerConversionTrait; @@ -163,4 +165,11 @@ final class Pattern extends ConfigEntityBase implements XbHttpApiEligibleConfigE ] + $other_values; } + /** + * {@inheritdoc} + */ + public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void { + // Nothing to do. + } + } diff --git a/src/Entity/XbHttpApiEligibleConfigEntityInterface.php b/src/Entity/XbHttpApiEligibleConfigEntityInterface.php index 4a9710cbbcb995bf78b1a3fb76e3bd1371489fdb..369433cd007090ec1a5e79b0173ec7a295da1965 100644 --- a/src/Entity/XbHttpApiEligibleConfigEntityInterface.php +++ b/src/Entity/XbHttpApiEligibleConfigEntityInterface.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Drupal\experience_builder\Entity; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\Query\QueryInterface; use Drupal\experience_builder\ClientSideRepresentation; /** @@ -33,4 +35,17 @@ interface XbHttpApiEligibleConfigEntityInterface extends ConfigEntityInterface { */ public static function denormalizeFromClientSide(array $data): array; + /** + * Allows the config entity query that generates the listing to be refined. + * + * @param \Drupal\Core\Entity\Query\QueryInterface $query + * The config entity query to refine, passed by reference. + * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability + * The cacheability of the given query, to be refined to match the + * refinements made to the query. + * + * @return void + */ + public static function refineListQuery(QueryInterface &$query, RefinableCacheableDependencyInterface $cacheability): void; + } diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php index b1a3de55a18c2092885f8503898d5a048a5d286d..a9ae5fadf8bfd46d9c8074a3f95501396ecba219 100644 --- a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php +++ b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php @@ -39,7 +39,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; */ #[ComponentSource( id: self::SOURCE_PLUGIN_ID, - label: new TranslatableMarkup('Blocks') + label: new TranslatableMarkup('Blocks'), + // While XB does not support context mappings yet, Block plugins also can + // contain logic and perform e.g. database queries that fetch data to present. + supportsImplicitInputs: TRUE, )] final class BlockComponent extends ComponentSourceBase implements ContainerFactoryPluginInterface { diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php index 80772366eb57ee0278b57d7640b0539adfa3dae8..19c12514eea946fd70e4edc825b6b35057a716aa 100644 --- a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php +++ b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php @@ -20,7 +20,8 @@ use Drupal\experience_builder\Entity\JavaScriptComponent; */ #[ComponentSource( id: self::SOURCE_PLUGIN_ID, - label: new TranslatableMarkup('Code Components') + label: new TranslatableMarkup('Code Components'), + supportsImplicitInputs: FALSE, )] final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase { @@ -146,6 +147,7 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase 'id' => self::SOURCE_PLUGIN_ID . '.' . $js_component->id(), 'label' => $js_component->label(), 'category' => '@todo', + 'provider' => NULL, 'source' => self::SOURCE_PLUGIN_ID, 'settings' => [ // @todo rename plugin_id in https://www.drupal.org/project/experience_builder/issues/3502982 diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php index 0ce82f540a526e040414f7750c9a55bec774215a..de628fd338af3a2c2cb4d802b053b6c1865e5484 100644 --- a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php +++ b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php @@ -27,7 +27,8 @@ use Symfony\Component\Filesystem\Path; */ #[ComponentSource( id: self::SOURCE_PLUGIN_ID, - label: new TranslatableMarkup('Single-Directory Components') + label: new TranslatableMarkup('Single-Directory Components'), + supportsImplicitInputs: FALSE, )] final class SingleDirectoryComponent extends GeneratedFieldExplicitInputUxComponentSourceBase implements UrlRewriteInterface { @@ -219,6 +220,7 @@ final class SingleDirectoryComponent extends GeneratedFieldExplicitInputUxCompon 'label' => $component_plugin->getPluginDefinition()['name'] ?? $component_plugin->getPluginId(), 'category' => $component_plugin->getPluginDefinition()['category'], 'source' => self::SOURCE_PLUGIN_ID, + 'provider' => $component_plugin->getPluginDefinition()['provider'], 'settings' => [ 'plugin_id' => $component_plugin->getPluginId(), 'prop_field_definitions' => $props, diff --git a/tests/src/Functional/PropSourceEndpointTest.php b/tests/src/Functional/PropSourceEndpointTest.php index 49bde26665fcb0744ffb20851bc5746681d0a000..1e5b07dcf05ba5db7ddb764b21625e8ede856ac2 100644 --- a/tests/src/Functional/PropSourceEndpointTest.php +++ b/tests/src/Functional/PropSourceEndpointTest.php @@ -58,12 +58,14 @@ class PropSourceEndpointTest extends FunctionalTestBase { 'announcements_feed:feed', 'comment_list', 'config:component_list', + 'config:core.extension', 'config:search.settings', 'config:system.menu.account', 'config:system.menu.admin', 'config:system.menu.footer', 'config:system.menu.main', 'config:system.site', + 'config:system.theme', 'config:views.view.comments_recent', 'config:views.view.content_recent', 'config:views.view.who_s_new', @@ -122,6 +124,7 @@ class PropSourceEndpointTest extends FunctionalTestBase { $this->assertArrayHasKey('name', $component); $this->assertArrayHasKey('category', $component); $this->assertArrayHasKey('source', $component); + $this->assertArrayHasKey('library', $component, $id); $this->assertArrayHasKey('default_markup', $component); $this->assertArrayHasKey('css', $component); $this->assertArrayHasKey('js_header', $component); @@ -130,6 +133,10 @@ class PropSourceEndpointTest extends FunctionalTestBase { $this->assertStringStartsWith('<!-- xb-start-', $data['block.system_menu_block.main']['default_markup']); $this->assertStringContainsString('--><nav role="navigation"', $data['block.system_menu_block.main']['default_markup']); + // Stark has no SDCs. + $this->assertSame('stark', $this->config('system.theme')->get('default')); + $this->assertArrayNotHasKey('sdc.olivero.teaser', $data); + $data = array_intersect_key( $data, [ @@ -163,6 +170,28 @@ class PropSourceEndpointTest extends FunctionalTestBase { self::assertEquals($extractValue, $data['sdc.sdc_test_all_props.all-props']['transforms']['test_integer']); self::assertEquals(['link' => []], $data['sdc.sdc_test_all_props.all-props']['transforms']['test_string_format_uri']); self::assertEquals(['mediaSelection' => [], 'mainProperty' => ['name' => 'target_id']], $data['sdc.sdc_test_all_props.all-props']['transforms']['test_object_drupal_image']); + + // Olivero does have an SDC, and it's enabled, but it is omitted because the + // default theme is Stark. + $this->assertInstanceOf(Component::class, Component::load('sdc.olivero.teaser')); + $this->assertTrue(Component::load('sdc.olivero.teaser')->status()); + $this->assertSame('olivero', Component::load('sdc.olivero.teaser')->get('provider')); + + // Change the default theme from Stark to Olivero, and observe the impact on + // the list of Components returned. + \Drupal::configFactory()->getEditable('system.theme')->set('default', 'olivero')->save(); + $this->rebuildAll(); + $this->drupalGet('xb/api/config/component'); + $data = Json::decode($page->getText()); + $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); + // Olivero does have an SDC! + $this->assertSame('olivero', $this->config('system.theme')->get('default')); + $this->assertArrayHasKey('sdc.olivero.teaser', $data); + // Repeated request is again a Dynamic Page Cache hit. + $this->drupalGet('xb/api/config/component'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); } /** diff --git a/tests/src/Kernel/Config/ComponentTest.php b/tests/src/Kernel/Config/ComponentTest.php index 76b468a3dbdded14d0fd4467576b613cd04fc961..bf241b6bc277fb19fea0e4a88d15ede671dedf60 100644 --- a/tests/src/Kernel/Config/ComponentTest.php +++ b/tests/src/Kernel/Config/ComponentTest.php @@ -103,6 +103,7 @@ class ComponentTest extends KernelTestBase { 'sdc' => [ 'component_config_entity_id' => 'sdc.sdc_test.my-cta', 'source' => SingleDirectoryComponent::SOURCE_PLUGIN_ID, + 'provider' => 'sdc_test', 'source_internal_id' => 'sdc_test:my-cta', 'expected_config_dependencies' => [ 'module' => [ @@ -116,6 +117,7 @@ class ComponentTest extends KernelTestBase { 'js' => [ 'component_config_entity_id' => 'js.my-cta', 'source' => JsComponent::SOURCE_PLUGIN_ID, + 'provider' => NULL, 'source_internal_id' => 'my-cta', 'expected_config_dependencies' => [ 'config' => [ @@ -133,7 +135,7 @@ class ComponentTest extends KernelTestBase { /** * @dataProvider providerComponentCreation */ - public function testComponentCreation(string $component_config_entity_id, string $source, string $source_internal_id, array $expected_config_dependencies): void { + public function testComponentCreation(string $component_config_entity_id, string $source, ?string $provider, string $source_internal_id, array $expected_config_dependencies): void { if ($source === JsComponent::SOURCE_PLUGIN_ID) { $this->assertEmpty(JavaScriptComponent::loadMultiple()); @@ -168,6 +170,7 @@ class ComponentTest extends KernelTestBase { 'label' => self::LABEL, 'category' => self::LABEL, 'source' => $source, + 'provider' => $provider, 'settings' => [ 'plugin_id' => $source_internal_id, 'prop_field_definitions' => [ diff --git a/tests/src/Kernel/Config/ComponentValidationTest.php b/tests/src/Kernel/Config/ComponentValidationTest.php index c802908e044f716dfa5692d996658748a1fb5e73..7680c983a87e9612315f01e5eb1bf9c3170cb58a 100644 --- a/tests/src/Kernel/Config/ComponentValidationTest.php +++ b/tests/src/Kernel/Config/ComponentValidationTest.php @@ -49,6 +49,13 @@ class ComponentValidationTest extends ConfigEntityValidationTestBase { ], ]; + /** + * {@inheritdoc} + */ + protected static array $propertiesWithOptionalValues = [ + 'provider', + ]; + /** * {@inheritdoc} */ diff --git a/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php b/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php index 61600aff168ea2317f1d174a542a490dab9e34aa..85b09283d0ae5308521bd5611d888bf2fdced466 100644 --- a/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php +++ b/tests/src/Kernel/Entity/JavascriptComponentStorageTest.php @@ -139,6 +139,7 @@ final class JavascriptComponentStorageTest extends KernelTestBase { $component = Component::load($component_id); self::assertInstanceOf(ComponentInterface::class, $component); + self::assertNull($component->get('provider')); self::assertEquals(['title'], \array_keys($component->getSettings()['prop_field_definitions'])); // Now update the js component and confirm we update the matching component.