From 8d555ae156926bb0ab5e473cb45f872e2846e491 Mon Sep 17 00:00:00 2001
From: Mikael Meulle <21535-just_like_good_vibes@users.noreply.drupalcode.org>
Date: Fri, 17 Jan 2025 17:07:44 +0000
Subject: [PATCH] Issue #3499625 by just_like_good_vibes, pdureau, dalemoore:
 New field prop entity source plugin

---
 src/Element/ComponentElementBuilder.php       |  2 +-
 .../EntityFieldSourceDeriverBase.php          |  3 +
 ...ceFieldPropertyDerivableContextDeriver.php | 35 ++++++++
 .../EntityReferencedDerivableContext.php      | 83 +++++++++++++++----
 .../Source/DerivableContextSourceBase.php     | 14 +++-
 .../EntityReferenceFieldPropertySource.php    | 69 +++++++++++++++
 .../Source/EntityReferencedSource.php         | 12 +++
 7 files changed, 201 insertions(+), 17 deletions(-)
 create mode 100644 src/Plugin/Derivative/EntityReferenceFieldPropertyDerivableContextDeriver.php
 create mode 100644 src/Plugin/UiPatterns/Source/EntityReferenceFieldPropertySource.php

diff --git a/src/Element/ComponentElementBuilder.php b/src/Element/ComponentElementBuilder.php
index 401f52925..7b0d85a46 100644
--- a/src/Element/ComponentElementBuilder.php
+++ b/src/Element/ComponentElementBuilder.php
@@ -40,7 +40,7 @@ class ComponentElementBuilder implements TrustedCallbackInterface {
    * Build component data provided to the SDC element.
    */
   public function build(array $element): array {
-    if (!isset($element['#ui_patterns'])) {
+    if (!isset($element['#ui_patterns']) || !isset($element['#component'])) {
       return $element;
     }
     $configuration = $element['#ui_patterns'];
diff --git a/src/Plugin/Derivative/EntityFieldSourceDeriverBase.php b/src/Plugin/Derivative/EntityFieldSourceDeriverBase.php
index fce8bfe0b..3201fc4b8 100644
--- a/src/Plugin/Derivative/EntityFieldSourceDeriverBase.php
+++ b/src/Plugin/Derivative/EntityFieldSourceDeriverBase.php
@@ -131,6 +131,7 @@ abstract class EntityFieldSourceDeriverBase extends DeriverBase implements Conta
         continue;
       }
       $field_storage_definition = $field_storage_definitions[$field_name];
+      $main_property_name = (is_object($field_storage_definition) && method_exists($field_storage_definition, "getMainPropertyName")) ? $field_storage_definition->getMainPropertyName() : NULL;
       $is_base = (in_array($field_name, $entityFieldsClassification["fields_base"]) || ($field_storage_definition instanceof BaseFieldDefinition));
       $returned[$field_name] = [
         "label" => $field_storage_definition->getLabel(),
@@ -143,6 +144,7 @@ abstract class EntityFieldSourceDeriverBase extends DeriverBase implements Conta
           "base" => $is_base,
           "cardinality" => $field_storage_definition->getCardinality(),
         ],
+        "main_property" => $main_property_name,
         'config_dependencies' => [],
         'properties' => [],
       ];
@@ -158,6 +160,7 @@ abstract class EntityFieldSourceDeriverBase extends DeriverBase implements Conta
             '@field' => $field_storage_definition->getLabel(),
           ]),
           "data_type" => $property_definition->getDataType(),
+          "entity_reference" => (($main_property_name === $property_id) && ($property_definition instanceof DataReferenceTargetDefinition)),
         ];
       }
     }
diff --git a/src/Plugin/Derivative/EntityReferenceFieldPropertyDerivableContextDeriver.php b/src/Plugin/Derivative/EntityReferenceFieldPropertyDerivableContextDeriver.php
new file mode 100644
index 000000000..281aaedce
--- /dev/null
+++ b/src/Plugin/Derivative/EntityReferenceFieldPropertyDerivableContextDeriver.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\ui_patterns\Plugin\Derivative;
+
+use Drupal\Component\Plugin\PluginBase;
+
+/**
+ * Provides derivable context for every field referencing an entity.
+ */
+class EntityReferenceFieldPropertyDerivableContextDeriver extends EntityFieldSourceDeriverBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDerivativeDefinitionsForEntityStorageFieldProperty(string $entity_type_id, string $field_name, string $property, array $base_plugin_derivative): void {
+    $id = implode(PluginBase::DERIVATIVE_SEPARATOR, [
+      $entity_type_id,
+      $field_name,
+      $property,
+    ]);
+    if (!$this->entityFieldsMetadata[$entity_type_id]["field_storages"][$field_name]["properties"][$property]['entity_reference']) {
+      return;
+    }
+    // $base_plugin_derivative["context_definitions"]["field_name"] =
+    $this->derivatives[$id] = array_merge(
+        $base_plugin_derivative,
+        [
+          "id" => $id,
+          "label" => $this->t("Field prop: entity"),
+          "context_requirements" => array_merge($base_plugin_derivative["context_requirements"], ["field_granularity:item"]),
+          "tags" => array_merge($base_plugin_derivative["tags"], ["xxx"]),
+        ]);
+  }
+
+}
diff --git a/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php b/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php
index 90e4b45cb..9451e7c93 100644
--- a/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php
+++ b/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php
@@ -14,6 +14,7 @@ use Drupal\Core\Plugin\Context\EntityContextDefinition;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ui_patterns\Attribute\DerivableContext;
 use Drupal\ui_patterns\DerivableContextPluginBase;
+use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
 use Drupal\ui_patterns\Plugin\Derivative\EntityReferencedDerivableContextDeriver;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -75,36 +76,90 @@ class EntityReferencedDerivableContext extends DerivableContextPluginBase {
    * {@inheritdoc}
    */
   public function getDerivedContexts(): array {
-    $contexts = $this->context;
-    // Base, entity_type, bundle, field name, target_entity_type, target_bundle.
-    $split_plugin_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $this->getPluginId());
-    [$bundle, $entity_type_id, $ref_field_name] = array_slice(array_reverse($split_plugin_id), 0, 3);
+    $referenced_entities = $this->getEntities();
+    $removed_context_keys = ["entity", "ui_patterns:field:", "bundle"];
+    $base_context = array_filter($this->context, function ($one_context, $one_context_id) use (&$removed_context_keys) {
+      if (in_array($one_context_id, $removed_context_keys)) {
+        return FALSE;
+      }
+      foreach ($removed_context_keys as $removed_context_key) {
+        if (str_starts_with($one_context_id, $removed_context_key)) {
+          return FALSE;
+        }
+      }
+      return TRUE;
+    }, ARRAY_FILTER_USE_BOTH);
+    $base_context = RequirementsContext::removeFromContext(["field_granularity:item"], $base_context);
+    $metadata = $this->getMetadata();
+    $entity_type_id = $metadata["entity_type_id"];
+    $bundle = $metadata["bundle"];
     // Bundle context definition.
     $bundle_context_definition = new ContextDefinition("string", "Bundle");
-    $contexts['bundle'] = new Context($bundle_context_definition, $bundle);
+    $base_context['bundle'] = new Context($bundle_context_definition, $bundle);
     // Entity context definition.
-    $entity = $this->context["entity"]->getContextValue();
     $entity_context_definition = new EntityContextDefinition($entity_type_id);
     if (!empty($bundle)) {
       $entity_context_definition->addConstraint('Bundle', [$bundle]);
     }
-    // Get referenced entities.
-    $referenced_entities = $this->getReferencedEntities($entity, $ref_field_name, $bundle);
-    if ((count($referenced_entities) === 0) && !$entity->id()) {
-      // Case when the entity is a sample (we are probably in a form)
-      // we generate a sample referenced entity.
-      $referenced_entities[] = $this->sampleEntityGenerator->get($entity_type_id, empty($bundle) ? $this->findEntityBundleWithField($entity_type_id, NULL) : $bundle);
-    }
+
     // Generate the contexts.
     $returned_contexts = [];
     foreach ($referenced_entities as $referenced_entity) {
-      $returned_contexts[] = array_merge($contexts, [
+      $returned_contexts[] = array_merge($base_context, [
         "entity" => new Context($entity_context_definition, $referenced_entity),
       ]);
     }
     return $returned_contexts;
   }
 
+  /**
+   * Get metadata about the current derivable context.
+   *
+   * @return array
+   *   Metadata about the current derivable context
+   */
+  protected function getMetadata() {
+    // Base, entity_type, bundle, field name, target_entity_type, target_bundle.
+    $split_plugin_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $this->getPluginId());
+    [$bundle, $entity_type_id, $ref_field_name] = array_slice(array_reverse($split_plugin_id), 0, 3);
+    // Bundle context definition.
+    return [
+      "entity_type_id" => $entity_type_id,
+      "bundle" => $bundle,
+      "field_name" => $ref_field_name,
+    ];
+  }
+
+  /**
+   * Get entities for this derivable context.
+   *
+   * @return array
+   *   The references entities.
+   */
+  protected function getEntities() : array {
+    $entity = $this->context["entity"]->getContextValue();
+    $metadata = $this->getMetadata();
+    $entity_type_id = $metadata["entity_type_id"];
+    $bundle = $metadata["bundle"];
+    // Get referenced entities.
+    $referenced_entities = $this->getReferencedEntities($entity, $metadata["field_name"], $bundle);
+    if ((count($referenced_entities) === 0) && !$entity->id()) {
+      // Case when the entity is a sample (we are probably in a form)
+      // we generate a sample referenced entity.
+      $referenced_entities[] = $this->sampleEntityGenerator->get($entity_type_id, empty($bundle) ? $this->findEntityBundleWithField($entity_type_id, NULL) : $bundle);
+    }
+    else {
+      // Check ui_patterns:field:index.
+      if (isset($this->context["ui_patterns:field:index"])) {
+        $field_index = $this->context["ui_patterns:field:index"]->getContextValue();
+        if (isset($referenced_entities[$field_index])) {
+          $referenced_entities = [$referenced_entities[$field_index]];
+        }
+      }
+    }
+    return $referenced_entities;
+  }
+
   /**
    * Get the referenced entities.
    *
diff --git a/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php b/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php
index a022b171c..e1103c9ef 100644
--- a/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php
+++ b/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php
@@ -104,6 +104,16 @@ abstract class DerivableContextSourceBase extends SourcePluginBase {
     return empty($returned) ? NULL : $returned;
   }
 
+  /**
+   * Get the context to pass to the derivable context plugin.
+   *
+   * @return array
+   *   The context.
+   */
+  protected function getContextForDerivation(): array {
+    return $this->context;
+  }
+
   /**
    * Set the source plugin according to configuration.
    *
@@ -117,7 +127,7 @@ abstract class DerivableContextSourceBase extends SourcePluginBase {
       return $this->sourcePlugins;
     }
     /** @var \Drupal\ui_patterns\DerivableContextInterface $derivable_context_plugin */
-    $derivable_context_plugin = $this->derivableContextManager->createInstance($derivable_context, DerivableContextPluginBase::buildConfiguration($this->context));
+    $derivable_context_plugin = $this->derivableContextManager->createInstance($derivable_context, DerivableContextPluginBase::buildConfiguration($this->getContextForDerivation()));
     if (!$derivable_context_plugin) {
       return $this->sourcePlugins;
     }
@@ -448,7 +458,7 @@ abstract class DerivableContextSourceBase extends SourcePluginBase {
    * @return array
    *   Definitions of blocks
    */
-  private function listDerivableContexts() : array {
+  protected function listDerivableContexts() : array {
     return $this->derivableContextManager->getDefinitionsMatchingContextsAndTags($this->context, $this->getDerivationTagFilter());
   }
 
diff --git a/src/Plugin/UiPatterns/Source/EntityReferenceFieldPropertySource.php b/src/Plugin/UiPatterns/Source/EntityReferenceFieldPropertySource.php
new file mode 100644
index 000000000..738ddb18f
--- /dev/null
+++ b/src/Plugin/UiPatterns/Source/EntityReferenceFieldPropertySource.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ui_patterns\Plugin\UiPatterns\Source;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\ui_patterns\Attribute\Source;
+use Drupal\ui_patterns\Plugin\Derivative\EntityReferenceFieldPropertyDerivableContextDeriver;
+
+/**
+ * Plugin implementation of the source.
+ */
+#[Source(
+  id: 'entity:field_property',
+  label: new TranslatableMarkup('Entity reference field property'),
+  description: new TranslatableMarkup('Entity reference field property source plugin for props.'),
+  deriver: EntityReferenceFieldPropertyDerivableContextDeriver::class,
+  context_definitions: [
+    'entity' => new ContextDefinition('entity', label: new TranslatableMarkup('Entity'), required: TRUE),
+  ]
+)]
+class EntityReferenceFieldPropertySource extends DerivableContextSourceBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
+    $form["derivable_context"]["#type"] = "hidden";
+    $form["derivable_context"]["#value"] = $form["derivable_context"]["#default_value"];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function listDerivableContexts() : array {
+    $derivable_contexts = parent::listDerivableContexts();
+    $field_name = $this->context['field_name']->getContextValue();
+    return array_filter($derivable_contexts, function ($derivable_context, $derivable_context_id) use ($field_name) {
+          return isset($derivable_context['metadata']) && is_array($derivable_context['metadata'])
+            && isset($derivable_context['metadata']['field_name']) && $derivable_context['metadata']['field_name'] === $field_name;
+    }, ARRAY_FILTER_USE_BOTH);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  protected function getSourcesTagFilter(): array {
+    return [
+      "widget:dismissible" => FALSE,
+      "widget" => FALSE,
+    ];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  protected function getDerivationTagFilter(): ?array {
+    return [
+      // "entity" => TRUE,
+      "entity_referenced" => TRUE,
+    ];
+  }
+
+}
diff --git a/src/Plugin/UiPatterns/Source/EntityReferencedSource.php b/src/Plugin/UiPatterns/Source/EntityReferencedSource.php
index d69e4f376..f5ec1a821 100644
--- a/src/Plugin/UiPatterns/Source/EntityReferencedSource.php
+++ b/src/Plugin/UiPatterns/Source/EntityReferencedSource.php
@@ -31,6 +31,18 @@ class EntityReferencedSource extends DerivableContextSourceBase {
     return $form;
   }
 
+  /**
+   * Get the context to pass to the derivable context plugin.
+   *
+   * @return array
+   *   The context.
+   */
+  protected function getContextForDerivation(): array {
+    return array_filter($this->context, function ($key) {
+      return $key !== 'ui_patterns:field:index';
+    }, ARRAY_FILTER_USE_KEY);
+  }
+
   /**
    * {@inheritDoc}
    */
-- 
GitLab