diff --git a/modules/ui_patterns_legacy/src/ComponentConverter.php b/modules/ui_patterns_legacy/src/ComponentConverter.php index 32e3a63151b4d044fb2f1bb43bb7d0c547ad51e4..d3de8269f43dab2849accdbe239a20f04f82ec3e 100644 --- a/modules/ui_patterns_legacy/src/ComponentConverter.php +++ b/modules/ui_patterns_legacy/src/ComponentConverter.php @@ -4,8 +4,8 @@ declare(strict_types = 1); namespace Drupal\ui_patterns_legacy; +use Drupal\Core\Render\Component\Exception\InvalidComponentDataException; use Drupal\sdc\Component\ComponentValidator; -use Drupal\sdc\Exception\InvalidComponentDataException; /** * Component converter. @@ -34,6 +34,9 @@ class ComponentConverter { $target = $this->addProperty('description', $source, 'description', $target); $target = $this->addProperty('category', $source, 'group', $target); $target = $this->addProperty('links', $source, 'links', $target); + $target = $this->addProperty('use', $source, '_template', $target); + $target = $this->addProperty('icon_map', $source, 'icon_map', $target); + $target = $this->addProperty('icon_path', $source, 'icon_path', $target); if (\array_key_exists('variants', $source)) { $target['variants'] = $this->getVariants($source); } diff --git a/modules/ui_patterns_legacy/src/Element/PatternPreview.php b/modules/ui_patterns_legacy/src/Element/PatternPreview.php index b65f4e51db0574cbed26166ef7d1b05ccdf0d041..9e52d9ee72b1e76453b3ff62d3b2b122b620fbfa 100644 --- a/modules/ui_patterns_legacy/src/Element/PatternPreview.php +++ b/modules/ui_patterns_legacy/src/Element/PatternPreview.php @@ -53,7 +53,7 @@ class PatternPreview extends Pattern { $slots = $story["slots"] ?? []; $props = $story["props"] ?? []; $slots = array_merge($element["#slots"], $slots); - $element["#slots"] = $manager::processStoriesSlots($slots); + $element["#slots"] = self::processStoriesSlots($slots); $element["#props"] = array_merge($element["#props"], $props); return $element; } diff --git a/modules/ui_patterns_library/src/Element/ComponentStory.php b/modules/ui_patterns_library/src/Element/ComponentStory.php index 4e68d5006f2e9fb3e2809c49a73b98c695e7fbd1..19dba5268c161a182ae4d9b5282a02a8ddb8b28b 100644 --- a/modules/ui_patterns_library/src/Element/ComponentStory.php +++ b/modules/ui_patterns_library/src/Element/ComponentStory.php @@ -50,7 +50,7 @@ class ComponentStory extends ComponentElement { } $story = $component["stories"][$story_id]; $slots = array_merge($element["#slots"] ?? [], $story["slots"] ?? []); - $element["#slots"] = $manager::processStoriesSlots($slots); + $element["#slots"] = self::processStoriesSlots($slots); $element["#props"] = array_merge($element["#props"] ?? [], $story["props"] ?? []); return $element; } diff --git a/src/ComponentPluginManager.php b/src/ComponentPluginManager.php new file mode 100644 index 0000000000000000000000000000000000000000..737cfaa21edc1e731e4260693d857fa8b49b09f0 --- /dev/null +++ b/src/ComponentPluginManager.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_patterns; + +use Drupal\Component\Plugin\CategorizingPluginManagerInterface; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Theme\ThemeManagerInterface; +use Drupal\sdc\Component\ComponentValidator; +use Drupal\sdc\Component\SchemaCompatibilityChecker; +use Drupal\sdc\ComponentNegotiator; +use Drupal\sdc\ComponentPluginManager as SdcPluginManager; +use Drupal\ui_patterns\SchemaManager\ReferencesResolver; + +/** + * UI Patterns extension of SDC component plugin manager. + */ +class ComponentPluginManager extends SdcPluginManager implements CategorizingPluginManagerInterface { + + /** + * Cache key prefix to use in the cache backend. + */ + const CACHE_KEY = 'ui_patterns'; + + /** + * {@inheritdoc} + */ + public function __construct( + ModuleHandlerInterface $module_handler, + ThemeHandlerInterface $themeHandler, + CacheBackendInterface $cacheBackend, + ConfigFactoryInterface $configFactory, + ThemeManagerInterface $themeManager, + ComponentNegotiator $componentNegotiator, + FileSystemInterface $fileSystem, + SchemaCompatibilityChecker $compatibilityChecker, + ComponentValidator $componentValidator, + string $appRoot, + protected PropTypePluginManager $propTypePluginManager, + protected ReferencesResolver $referencesSolver, + ) { + parent::__construct( + $module_handler, + $themeHandler, + $cacheBackend, + $configFactory, + $themeManager, + $componentNegotiator, + $fileSystem, + $compatibilityChecker, + $componentValidator, + $appRoot + ); + $this->setCacheBackend($cacheBackend, self::CACHE_KEY); + } + + /** + * {@inheritdoc} + */ + protected function alterDefinitions(&$definitions) { + parent::alterDefinitions($definitions); + foreach ($definitions as $component_id => $definition) { + $definition = $this->annotateProps($definition); + $definition = $this->resolveTemplatePath($definition); + $definitions[$component_id] = $definition; + } + } + + /** + * Annotate each prop in a component definition. + * + * This is the main purpose of overriding SDC component plugin manager. + * We add a 'ui_patterns' object in each prop schema of the definition. + */ + protected function annotateProps(array $definition): array { + if (!isset($definition['props'])) { + return $definition; + } + if (!isset($definition['props']['properties'])) { + return $definition; + } + foreach ($definition['props']['properties'] as $prop_id => $prop) { + $prop_type = $this->propTypePluginManager->getPropTypePlugin($prop); + if (isset($prop['$ref']) && str_starts_with($prop['$ref'], "ui-patterns://")) { + // Resolve prop schema here, because: + // - Drupal\sdc\Component\ComponentValidator::getClassProps() is + // executed before schema references are resolved, so SDC believe + // a reference is a PHP namespace. + // - It is not possible to propose a patch to SDC because + // SchemaStorage::resolveRefSchema() is not recursively resolving + // the schemas anyway. + $prop = $this->referencesSolver->resolve($prop); + } + $prop['ui_patterns']['type_definition'] = $prop_type; + $prop['ui_patterns']["summary"] = $prop_type->getSummary($prop); + $definition['props']['properties'][$prop_id] = $prop; + } + return $definition; + } + + /** + * Resolve template path. + * + * SDC is not allowing to have a custom template property because it is always + * overriding its value. So we use _template instead. + * See: https://www.drupal.org/project/drupal/issues/3390717 + */ + protected function resolveTemplatePath(array $definition): array { + if (!isset($definition["_template"])) { + return $definition; + } + $definition["template"] = $definition["_template"]; + unset($definition["_template"]); + if (str_starts_with($definition["template"], "@")) { + // @todo resolve namespace. + } + return $definition; + } + + /** + * {@inheritdoc} + */ + public function getCategories() { + // Fetch all categories from definitions and remove duplicates. + $categories = array_unique(array_values(array_map(function ($definition) { + return $definition['group']; + }, $this->getDefinitions()))); + natcasesort($categories); + return $categories; + } + + /** + * {@inheritdoc} + */ + public function getSortedDefinitions(array $definitions = NULL) { + // Sort the plugins first by category, then by label. + $definitions = $definitions ?? $this->getDefinitions(); + uasort($definitions, function ($a, $b) use ($label_key) { + if ((string) $a['group'] != (string) $b['group']) { + return strnatcasecmp($a['group'], $b['group']); + } + return strnatcasecmp($a[$label_key], $b[$label_key]); + }); + return $definitions; + } + + /** + * {@inheritdoc} + */ + public function getGroupedDefinitions(?array $definitions = NULL): array { + $definitions = $definitions ?: $this->getDefinitions(); + $groups = []; + foreach ($definitions as $id => $definition) { + $group = $definition["group"] ?? "Other"; + $groups[$group][$id] = $definition; + } + return $groups; + } + +} diff --git a/src/Element/ComponentElement.php b/src/Element/ComponentElement.php index 6d73416363f126da17400d5bda776d799084e53a..edca327e2c022076c8d4729b1225bd6e51f0f561 100644 --- a/src/Element/ComponentElement.php +++ b/src/Element/ComponentElement.php @@ -5,8 +5,8 @@ declare(strict_types = 1); namespace Drupal\ui_patterns\Element; use Drupal\Component\Render\MarkupInterface; +use Drupal\Core\Render\Element\ComponentElement as SdcComponentElement; use Drupal\Core\Render\RenderableInterface; -use Drupal\sdc\Element\ComponentElement as SdcComponentElement; /** * Empty for now. Act as a proxy between SDC element and our additions. @@ -79,4 +79,38 @@ class ComponentElement extends SdcComponentElement { return $slot; } + /** + * Process stories slots. + * + * Stories slots have no "#" prefix in render arrays. Let's add them. + * A bit like UI Patterns 1.x's PatternPreview::getPreviewMarkup() + * This method belongs here because used by both ui_patterns_library and + * ui_patterns_legacy. + */ + public static function processStoriesSlots(array $slots): array { + foreach ($slots as $slot_id => $slot) { + if (!is_array($slot)) { + continue; + } + if (array_is_list($slot)) { + $slots[$slot_id] = self::processStoriesSlots($slot); + } + $slot_keys = array_keys($slot); + $render_keys = ["theme", "type", "markup", "plain_text"]; + if (count(array_intersect($slot_keys, $render_keys)) > 0) { + foreach ($slot as $key => $value) { + if (is_array($value)) { + $value = self::processStoriesSlots($value); + } + if (str_starts_with($key, "#")) { + continue; + } + $slots[$slot_id]["#" . $key] = $value; + unset($slots[$slot_id][$key]); + } + } + } + return $slots; + } + } diff --git a/src/Sdc/ComponentPluginManagerDecorator.php b/src/Sdc/ComponentPluginManagerDecorator.php deleted file mode 100644 index 6cf252d68d6a6f7041b66cf215fe1266f1ef9e3a..0000000000000000000000000000000000000000 --- a/src/Sdc/ComponentPluginManagerDecorator.php +++ /dev/null @@ -1,122 +0,0 @@ -<?php - -declare(strict_types = 1); - -namespace Drupal\ui_patterns\Sdc; - -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Extension\ThemeHandlerInterface; -use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Theme\ThemeManagerInterface; -use Drupal\sdc\Component\ComponentValidator; -use Drupal\sdc\Component\SchemaCompatibilityChecker; -use Drupal\sdc\ComponentNegotiator; -use Drupal\sdc\ComponentPluginManager; -use Drupal\sdc\Plugin\Component; -use Drupal\ui_patterns\PropTypePluginManager; -use Drupal\ui_patterns\SourcePluginManager; - -/** - * Plugin Manager for *.ui_patterns.yml configuration files. - * - * Plugin Manager overwrites getDiscovery() to provide a decorated - * Discovery. Decoration of the service seems not possible for me. - * Probably there is more gentle way. - * - * @see plugin_api - * - * @internal - */ -abstract class ComponentPluginManagerDecorator extends ComponentPluginManager { - - /** - * - */ - public function __construct( - protected ComponentPluginManager $parentSdcPluginManager, - protected PropTypePluginManager $propTypePluginManager, - protected SourcePluginManager $sourcePluginManager, - ModuleHandlerInterface $module_handler, - ThemeHandlerInterface $themeHandler, - CacheBackendInterface $cacheBackend, - ConfigFactoryInterface $configFactory, - ThemeManagerInterface $themeManager, - ComponentNegotiator $componentNegotiator, - FileSystemInterface $fileSystem, - SchemaCompatibilityChecker $compatibilityChecker, - ComponentValidator $componentValidator, - string $appRoot, - ) { - parent::__construct( - $module_handler, - $themeHandler, - $cacheBackend, - $configFactory, - $themeManager, - $componentNegotiator, - $fileSystem, - $compatibilityChecker, - $componentValidator, - $appRoot - ); - $this->setCacheBackend($cacheBackend, $this->getCacheKey()); - } - - /** - * - */ - public function createInstance($plugin_id, array $configuration = []): Component { - if (parent::hasDefinition($plugin_id)) { - return parent::createInstance($plugin_id, $configuration); - } - else { - return $this->parentSdcPluginManager->createInstance($plugin_id, $configuration); - } - } - - /** - * Returns the cache key for the decorated service. - * - * @return string - * The cache key. - */ - abstract protected function getCacheKey(); - - /** - * {@inheritdoc} - */ - public function find(string $component_id): Component { - if (parent::hasDefinition($component_id)) { - return parent::find($component_id); - } - return $this->parentSdcPluginManager->find($component_id); - } - - /** - * {@inheritdoc} - */ - public function getAllComponents(): array { - $original_components = $this->parentSdcPluginManager->getAllComponents(); - return array_merge($original_components, parent::getAllComponents()); - } - - /** - * {@inheritdoc} - */ - public function getDefinitions() { - $decorated_definitions = parent::getDefinitions(); - $original_definitions = $this->parentSdcPluginManager->getDefinitions(); - return array_merge($original_definitions, $decorated_definitions); - } - - /** - * {@inheritdoc} - */ - public function getDefinition($plugin_id, $exception_on_invalid = TRUE) { - $original_definition = parent::getDefinition($plugin_id, FALSE); - return $original_definition ?? $this->parentSdcPluginManager->getDefinition($plugin_id, $exception_on_invalid); - } - -} diff --git a/src/Sdc/UiPatternsSdcPluginManager.php b/src/Sdc/UiPatternsSdcPluginManager.php deleted file mode 100644 index 5759a65ffdf032a189deb501495f4e17034476db..0000000000000000000000000000000000000000 --- a/src/Sdc/UiPatternsSdcPluginManager.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php - -declare(strict_types = 1); - -namespace Drupal\ui_patterns\Sdc; - -/** - * Plugin Manager for.... - * - * @see plugin_api - * - * @internal - */ -class UiPatternsSdcPluginManager extends ComponentPluginManagerDecorator { - - /** - * {@inheritdoc} - */ - protected function getCacheKey() { - return 'ui_patterns'; - } - - /** - * {@inheritdoc} - */ - protected function alterDefinitions(&$definitions) { - parent::alterDefinitions($definitions); - // @todo injection. - $resolver = \Drupal::service('ui_patterns.schema_reference_solver'); - foreach ($definitions as $component_id => $definition) { - if (!isset($definition['props'])) { - continue; - } - - if (!isset($definition['props']['properties'])) { - continue; - } - foreach ($definition['props']['properties'] as $prop_id => $prop) { - $prop = $this->replacePhpNamespace($prop); - $prop_type = $this->propTypePluginManager->getPropTypePlugin($prop); - if (isset($prop['$ref']) && str_starts_with($prop['$ref'], "ui-patterns://")) { - // Resolve prop schema here, because: - // - Drupal\sdc\Component\ComponentValidator::getClassProps() is - // executed before schema references are resolved, so SDC believe - // a reference is a PHP namespace. - // - It is not possible to propose a patch to SDC because - // SchemaStorage::resolveRefSchema() is not recursively resolving - // the schemas anyway. - $prop = $resolver->resolve($prop); - } - $prop['ui_patterns']['type_definition'] = $prop_type; - $prop['ui_patterns']["summary"] = $prop_type->getSummary($prop); - $definition['props']['properties'][$prop_id] = $prop; - } - $definitions[$component_id] = $definition; - } - } - - /** - * Replace PHP namespace in schema's type. - * - * SDC allows namespaced PHP classes as prop types. This is not compliant with - * JSON schema, and props using this are ignored by SDC during JSON schema - * validation. So let replace them. - */ - protected function replacePhpNamespace(array $schema): array { - if (!isset($schema['type'])) { - return $schema; - } - if (!is_string($schema['type'])) { - return $schema; - } - $mappings = [ - 'Drupal\Core\Template\Attribute' => "ui-patterns://attributes", - '\Drupal\Core\Template\Attribute' => "ui-patterns://attributes", - ]; - foreach ($mappings as $namespace => $ref) { - if ($schema['type'] == $namespace) { - unset($schema['type']); - $schema['$ref'] = $ref; - return $schema; - } - } - return $schema; - } - - /** - * {@inheritdoc} - */ - public function getGroupedDefinitions(?array $definitions = NULL): array { - $definitions = $definitions ?: $this->getDefinitions(); - $groups = []; - foreach ($definitions as $id => $definition) { - $group = $definition["group"] ?? "Other"; - $groups[$group][$id] = $definition; - } - return $groups; - } - - /** - * Process stories slots. - * - * Stories slots have no "#" prefix in render arrays. Let's add them. - * A bit like UI Patterns 1.x's PatternPreview::getPreviewMarkup() - * This method belongs here because used by both ui_patterns_library and - * ui_patterns_legacy. - */ - public static function processStoriesSlots(array $slots): array { - foreach ($slots as $slot_id => $slot) { - if (!is_array($slot)) { - continue; - } - if (array_is_list($slot)) { - $slots[$slot_id] = self::processStoriesSlots($slot); - } - $slot_keys = array_keys($slot); - $render_keys = ["theme", "type", "markup", "plain_text"]; - if (count(array_intersect($slot_keys, $render_keys)) > 0) { - foreach ($slot as $key => $value) { - if (is_array($value)) { - $value = self::processStoriesSlots($value); - } - if (str_starts_with($key, "#")) { - continue; - } - $slots[$slot_id]["#" . $key] = $value; - unset($slots[$slot_id][$key]); - } - } - } - return $slots; - } - -} diff --git a/tests/modules/ui_patterns_test/components/button/button.component.yml b/tests/modules/ui_patterns_test/components/button/button.component.yml index 9bc44c329bcef8af80c99ed1d57dd939f195b462..b2b75579c92bcf6735b584b7ce1f5da64e94f078 100644 --- a/tests/modules/ui_patterns_test/components/button/button.component.yml +++ b/tests/modules/ui_patterns_test/components/button/button.component.yml @@ -4,7 +4,7 @@ description: "For actions in forms, dialogs, and more with support for multiple links: - "https://getbootstrap.com/docs/5.3/components/buttons/" group: "Button" -template: pattern-button.html.twig +_template: ../../components/button/pattern-button.html.twig variants: default: title: "Default" diff --git a/tests/modules/ui_patterns_test/components/button/button.twig b/tests/modules/ui_patterns_test/components/button/button.twig deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/modules/ui_patterns_test/components/progress/progress.component.yml b/tests/modules/ui_patterns_test/components/progress/progress.component.yml index bab53b35dece2c5380468b5a882b41cce89f14f4..06e31808140e540f6e23d31452e4088b18118a39 100644 --- a/tests/modules/ui_patterns_test/components/progress/progress.component.yml +++ b/tests/modules/ui_patterns_test/components/progress/progress.component.yml @@ -3,7 +3,7 @@ name: "Progress" description: "The progress element displays an indicator showing the completion progress of a task, typically in the form of a bar. Progress components are built with two HTML elements, some CSS to set the width, and a few attributes. Bootstrap does not use the HTML5 <progress> element, ensuring you can stack progress bars, animate them, and place text labels over them." links: - "https://getbootstrap.com/docs/5.3/components/progress/" -template: path/to/template/progress.twig +_template: path/to/template/progress.twig variants: default: title: "Default" diff --git a/tests/modules/ui_patterns_test/components/progress/progress.twig b/tests/modules/ui_patterns_test/components/progress/progress.twig deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/ui_patterns.services.yml b/ui_patterns.services.yml index cd39e79bedebc51407e482151084773419c96ea2..0a1de5144aed5780300bed9db546c90385d435dd 100644 --- a/ui_patterns.services.yml +++ b/ui_patterns.services.yml @@ -1,26 +1,8 @@ services: # Plugins managers. - plugin.manager.ui_patterns_prop_type: - class: Drupal\ui_patterns\PropTypePluginManager - arguments: - - "@container.namespaces" - - "@cache.discovery" - - "@module_handler" - - '@Drupal\sdc\Component\SchemaCompatibilityChecker' - plugin.manager.ui_patterns_source: - class: Drupal\ui_patterns\SourcePluginManager - parent: default_plugin_manager - - # SDC extension. - ui_patterns.plugin.manager.sdc: - class: Drupal\ui_patterns\Sdc\UiPatternsSdcPluginManager - decorates: plugin.manager.sdc - decoration_priority: 9 - public: false + plugin.manager.sdc: + class: Drupal\ui_patterns\ComponentPluginManager arguments: - - "@ui_patterns.plugin.manager.sdc.inner" - - "@plugin.manager.ui_patterns_prop_type" - - "@plugin.manager.ui_patterns_source" - "@module_handler" - "@theme_handler" - "@cache.discovery" @@ -31,6 +13,18 @@ services: - '@Drupal\sdc\Component\SchemaCompatibilityChecker' - '@Drupal\sdc\Component\ComponentValidator' - "%app.root%" + - "@plugin.manager.ui_patterns_prop_type" + - "@ui_patterns.schema_reference_solver" + plugin.manager.ui_patterns_prop_type: + class: Drupal\ui_patterns\PropTypePluginManager + arguments: + - "@container.namespaces" + - "@cache.discovery" + - "@module_handler" + - '@Drupal\sdc\Component\SchemaCompatibilityChecker' + plugin.manager.ui_patterns_source: + class: Drupal\ui_patterns\SourcePluginManager + parent: default_plugin_manager # Builders ui_patterns.component_element_builder: