From b71771dd275f663be4bfb274c748f8dcc6bccfef Mon Sep 17 00:00:00 2001
From: Pierre Dureau <31905-pdureau@users.noreply.drupalcode.org>
Date: Thu, 14 Nov 2024 11:06:34 +0000
Subject: [PATCH] Issue #3486547 by pdureau, just_like_good_vibes: add new
 EnumSetPropType

---
 .../ui_patterns_legacy/src/PropConverter.php  |  1 +
 src/EnumTrait.php                             | 61 +++++++++++++++++++
 .../UiPatterns/PropType/EnumListPropType.php  | 24 ++++----
 .../UiPatterns/PropType/EnumPropType.php      | 11 ++--
 .../UiPatterns/PropType/EnumSetPropType.php   | 52 ++++++++++++++++
 .../UiPatterns/Source/CheckboxesWidget.php    | 25 ++------
 .../UiPatterns/Source/EnumSourceTrait.php     | 32 ----------
 src/Plugin/UiPatterns/Source/SelectWidget.php | 23 +------
 .../UiPatterns/Source/SelectsWidget.php       | 56 +++++++++++++++++
 src/PropTypePluginBase.php                    |  7 +--
 src/SchemaManager/CompatibilityChecker.php    |  7 ++-
 11 files changed, 203 insertions(+), 96 deletions(-)
 create mode 100644 src/EnumTrait.php
 create mode 100644 src/Plugin/UiPatterns/PropType/EnumSetPropType.php
 delete mode 100644 src/Plugin/UiPatterns/Source/EnumSourceTrait.php
 create mode 100644 src/Plugin/UiPatterns/Source/SelectsWidget.php

diff --git a/modules/ui_patterns_legacy/src/PropConverter.php b/modules/ui_patterns_legacy/src/PropConverter.php
index 2691949ad..cbf76ef98 100644
--- a/modules/ui_patterns_legacy/src/PropConverter.php
+++ b/modules/ui_patterns_legacy/src/PropConverter.php
@@ -51,6 +51,7 @@ class PropConverter {
     $labels = \array_values($setting['options']);
     $prop = [
       'type' => 'array',
+      'uniqueItems' => TRUE,
       'items' => [
         'type' => $this->getEnumType($values),
         'enum' => $values,
diff --git a/src/EnumTrait.php b/src/EnumTrait.php
new file mode 100644
index 000000000..3d8bbfb16
--- /dev/null
+++ b/src/EnumTrait.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ui_patterns;
+
+/**
+ * Trait for plugins (sources and prop types) handling enum values.
+ */
+trait EnumTrait {
+
+  /**
+   * Get form element options from enumeration.
+   */
+  protected function getEnumOptions(array $definition): array {
+    $values = array_combine($definition['enum'], $definition['enum']);
+    foreach ($values as $key => $label) {
+      if (is_string($label)) {
+        $values[$key] = ucwords($label);
+      }
+    }
+    if (!isset($definition['meta:enum'])) {
+      return array_values($values);
+    }
+    $meta = $definition['meta:enum'];
+    // Remove meta:enum items not found in options.
+    $meta = array_intersect_key($meta, $values);
+    foreach ($meta as $value => $label) {
+      $values[$value] = $label;
+    }
+    return $values;
+  }
+
+  /**
+   * Get allowed values from enumeration.
+   */
+  protected function getAllowedValues(array $definition): array {
+    return array_values($this->getEnumOptions($definition));
+  }
+
+  /**
+   * Converts a source value type to enum data type.
+   *
+   * @param string $value
+   *   The stored.
+   * @param array $enum
+   *   The defined enums.
+   *
+   * @return float|int|mixed
+   *   The converted value.
+   */
+  protected function convertValueToEnumType(string $value, array $enum) {
+    return match (TRUE) {
+      in_array($value, $enum, TRUE) => $value,
+      in_array((int) $value, $enum, TRUE)  => (int) $value,
+      in_array((float) $value, $enum, TRUE) => (float) $value,
+      default => $value,
+    };
+  }
+
+}
diff --git a/src/Plugin/UiPatterns/PropType/EnumListPropType.php b/src/Plugin/UiPatterns/PropType/EnumListPropType.php
index 4877f4084..b639622ee 100644
--- a/src/Plugin/UiPatterns/PropType/EnumListPropType.php
+++ b/src/Plugin/UiPatterns/PropType/EnumListPropType.php
@@ -6,6 +6,7 @@ namespace Drupal\ui_patterns\Plugin\UiPatterns\PropType;
 
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ui_patterns\Attribute\PropType;
+use Drupal\ui_patterns\EnumTrait;
 use Drupal\ui_patterns\PropTypePluginBase;
 
 /**
@@ -16,28 +17,27 @@ use Drupal\ui_patterns\PropTypePluginBase;
   label: new TranslatableMarkup('List of enums'),
   description: new TranslatableMarkup('Ordered list of predefined string or number items.'),
   default_source: 'checkboxes',
-  schema: ['type' => 'array', 'items' => ['type' => ['string', 'number', 'integer'], 'enum' => []]]
+  schema: ['type' => 'array', 'items' => ['type' => ['string', 'number', 'integer'], 'enum' => []]],
+  priority: 1
 )]
 class EnumListPropType extends PropTypePluginBase {
 
+  use EnumTrait;
+
   /**
    * {@inheritdoc}
    */
   public function getSummary(array $definition): array {
     $summary = parent::getSummary($definition);
-    if (isset($definition['items']['enum']) && !isset($definition['items']['meta:enum'])) {
-      $values = implode(", ", $definition['items']['enum']);
-      $summary[] = $this->t("Values: @values", ["@values" => $values]);
-    }
-    if (isset($definition['items']['enum']) && isset($definition['items']['meta:enum'])) {
-      $values = implode(", ", $definition['items']['meta:enum']);
-      $summary[] = $this->t("Values: @values", ["@values" => $values]);
+    if (isset($definition['items']['enum'])) {
+      $values = implode(", ", $this->getAllowedValues($definition['items']));
+      $summary[] = $this->t("Allowed values: @values", ["@values" => $values]);
     }
-    if (isset($definition['items']['minItems'])) {
-      $summary[] = $this->t("Min items: @length", ["@length" => $definition['items']['minItems']]);
+    if (isset($definition['minItems'])) {
+      $summary[] = $this->t("Min items: @length", ["@length" => $definition['minItems']]);
     }
-    if (isset($definition['items']['maxItems'])) {
-      $summary[] = $this->t("Max items: @length", ["@length" => $definition['items']['maxItems']]);
+    if (isset($definition['maxItems'])) {
+      $summary[] = $this->t("Max items: @length", ["@length" => $definition['maxItems']]);
     }
     return $summary;
   }
diff --git a/src/Plugin/UiPatterns/PropType/EnumPropType.php b/src/Plugin/UiPatterns/PropType/EnumPropType.php
index 8e64c2b65..3c143449a 100644
--- a/src/Plugin/UiPatterns/PropType/EnumPropType.php
+++ b/src/Plugin/UiPatterns/PropType/EnumPropType.php
@@ -6,6 +6,7 @@ namespace Drupal\ui_patterns\Plugin\UiPatterns\PropType;
 
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ui_patterns\Attribute\PropType;
+use Drupal\ui_patterns\EnumTrait;
 use Drupal\ui_patterns\PropTypePluginBase;
 
 /**
@@ -21,16 +22,16 @@ use Drupal\ui_patterns\PropTypePluginBase;
 )]
 class EnumPropType extends PropTypePluginBase {
 
+  use EnumTrait;
+
   /**
    * {@inheritdoc}
    */
   public function getSummary(array $definition): array {
     $summary = parent::getSummary($definition);
-    if (isset($definition['enum']) && !isset($definition['meta:enum'])) {
-      $summary[] = $this->t("Values: @values", ["@values" => implode(", ", $definition['enum'])]);
-    }
-    if (isset($definition['enum']) && isset($definition['meta:enum'])) {
-      $summary[] = $this->t("Values: @values", ["@values" => implode(", ", $definition['meta:enum'])]);
+    if (isset($definition['enum'])) {
+      $values = implode(", ", $this->getAllowedValues($definition));
+      $summary[] = $this->t("Allowed values: @values", ["@values" => $values]);
     }
     return $summary;
   }
diff --git a/src/Plugin/UiPatterns/PropType/EnumSetPropType.php b/src/Plugin/UiPatterns/PropType/EnumSetPropType.php
new file mode 100644
index 000000000..58d158be4
--- /dev/null
+++ b/src/Plugin/UiPatterns/PropType/EnumSetPropType.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ui_patterns\Plugin\UiPatterns\PropType;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\ui_patterns\Attribute\PropType;
+use Drupal\ui_patterns\EnumTrait;
+use Drupal\ui_patterns\PropTypePluginBase;
+
+/**
+ * Provides a 'enum_set' PropType.
+ */
+#[PropType(
+  id: 'enum_set',
+  label: new TranslatableMarkup('Set of enums'),
+  description: new TranslatableMarkup('Set of unique predefined string or number items.'),
+  default_source: 'checkboxes',
+  schema: [
+    'type' => 'array',
+    'uniqueItems' => TRUE,
+    'items' => [
+      'type' => ['string', 'number', 'integer'],
+      'enum' => [],
+    ],
+  ],
+  priority: 10
+)]
+class EnumSetPropType extends PropTypePluginBase {
+
+  use EnumTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary(array $definition): array {
+    $summary = parent::getSummary($definition);
+    if (isset($definition['items']['enum'])) {
+      $values = implode(", ", $this->getAllowedValues($definition['items']));
+      $summary[] = $this->t("Allowed values: @values", ["@values" => $values]);
+    }
+    if (isset($definition['minItems'])) {
+      $summary[] = $this->t("Min items: @length", ["@length" => $definition['minItems']]);
+    }
+    if (isset($definition['maxItems'])) {
+      $summary[] = $this->t("Max items: @length", ["@length" => $definition['maxItems']]);
+    }
+    return $summary;
+  }
+
+}
diff --git a/src/Plugin/UiPatterns/Source/CheckboxesWidget.php b/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
index 8c5527466..3f6fee4b0 100644
--- a/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
+++ b/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
@@ -7,6 +7,7 @@ namespace Drupal\ui_patterns\Plugin\UiPatterns\Source;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ui_patterns\Attribute\Source;
+use Drupal\ui_patterns\EnumTrait;
 use Drupal\ui_patterns\SourcePluginPropValue;
 
 /**
@@ -16,11 +17,13 @@ use Drupal\ui_patterns\SourcePluginPropValue;
   id: 'checkboxes',
   label: new TranslatableMarkup('Checkboxes'),
   description: new TranslatableMarkup('A set of checkboxes.'),
-  prop_types: ['enum_list'],
+  prop_types: ['enum_set'],
   tags: ['widget']
 )]
 class CheckboxesWidget extends SourcePluginPropValue {
 
+  use EnumTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -38,28 +41,10 @@ class CheckboxesWidget extends SourcePluginPropValue {
     $form['value'] = [
       '#type' => 'checkboxes',
       '#default_value' => $this->getSetting('value') ?? [],
-      "#options" => $this->getOptions(),
+      "#options" => $this->getEnumOptions($this->propDefinition['items']),
     ];
     $this->addRequired($form['value']);
     return $form;
   }
 
-  /**
-   * Get checkboxes options.
-   */
-  protected function getOptions(): array {
-    $options = array_combine($this->propDefinition['items']['enum'], $this->propDefinition['items']['enum']);
-    foreach ($options as $key => $label) {
-      if (is_string($label)) {
-        $options[$key] = ucwords($label);
-      }
-    }
-    if (!isset($this->propDefinition['items']['meta:enum'])) {
-      return $options;
-    }
-    $meta = $this->propDefinition['items']['meta:enum'];
-    // Remove meta:enum items not found in options.
-    return array_intersect_key($meta, $options);
-  }
-
 }
diff --git a/src/Plugin/UiPatterns/Source/EnumSourceTrait.php b/src/Plugin/UiPatterns/Source/EnumSourceTrait.php
deleted file mode 100644
index 8f1a03a0f..000000000
--- a/src/Plugin/UiPatterns/Source/EnumSourceTrait.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\ui_patterns\Plugin\UiPatterns\Source;
-
-/**
- * Trait for sources handling enum values.
- */
-trait EnumSourceTrait {
-
-  /**
-   * Converts a source value type to enum data type.
-   *
-   * @param string $value
-   *   The stored.
-   * @param array $enum
-   *   The defined enums.
-   *
-   * @return float|int|mixed
-   *   The converted value.
-   */
-  protected function convertValueToEnumType(string $value, array $enum) {
-    return match (TRUE) {
-      in_array($value, $enum, TRUE) => $value,
-      in_array((int) $value, $enum, TRUE)  => (int) $value,
-      in_array((float) $value, $enum, TRUE) => (float) $value,
-      default => $value,
-    };
-  }
-
-}
diff --git a/src/Plugin/UiPatterns/Source/SelectWidget.php b/src/Plugin/UiPatterns/Source/SelectWidget.php
index 717a286ef..e593bdfcb 100644
--- a/src/Plugin/UiPatterns/Source/SelectWidget.php
+++ b/src/Plugin/UiPatterns/Source/SelectWidget.php
@@ -7,6 +7,7 @@ namespace Drupal\ui_patterns\Plugin\UiPatterns\Source;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ui_patterns\Attribute\Source;
+use Drupal\ui_patterns\EnumTrait;
 use Drupal\ui_patterns\SourcePluginPropValue;
 
 /**
@@ -21,7 +22,7 @@ use Drupal\ui_patterns\SourcePluginPropValue;
 )]
 class SelectWidget extends SourcePluginPropValue {
 
-  use EnumSourceTrait;
+  use EnumTrait;
 
   /**
    * {@inheritdoc}
@@ -31,7 +32,7 @@ class SelectWidget extends SourcePluginPropValue {
     $form['value'] = [
       '#type' => 'select',
       '#default_value' => $this->getSetting('value'),
-      "#options" => $this->getOptions(),
+      "#options" => $this->getEnumOptions($this->propDefinition),
       "#empty_option" => $this->t("- Select -"),
     ];
     $this->addRequired($form['value']);
@@ -41,24 +42,6 @@ class SelectWidget extends SourcePluginPropValue {
     return $form;
   }
 
-  /**
-   * Get select options.
-   */
-  protected function getOptions(): array {
-    $options = array_combine($this->propDefinition['enum'], $this->propDefinition['enum']);
-    foreach ($options as $key => $label) {
-      if (is_string($label)) {
-        $options[$key] = ucwords($label);
-      }
-    }
-    if (!isset($this->propDefinition['meta:enum'])) {
-      return $options;
-    }
-    $meta = $this->propDefinition['meta:enum'];
-    // Remove meta:enum items not found in options.
-    return array_intersect_key($meta, $options);
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/UiPatterns/Source/SelectsWidget.php b/src/Plugin/UiPatterns/Source/SelectsWidget.php
new file mode 100644
index 000000000..3543aae8d
--- /dev/null
+++ b/src/Plugin/UiPatterns/Source/SelectsWidget.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ui_patterns\Plugin\UiPatterns\Source;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\ui_patterns\Attribute\Source;
+use Drupal\ui_patterns\EnumTrait;
+use Drupal\ui_patterns\SourcePluginPropValue;
+
+/**
+ * Plugin implementation of the source.
+ */
+#[Source(
+  id: 'selects',
+  label: new TranslatableMarkup('Selects'),
+  description: new TranslatableMarkup('A set of select.'),
+  prop_types: ['enum_list'],
+  tags: ['widget']
+)]
+class SelectsWidget extends SourcePluginPropValue {
+
+  use EnumTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropValue(): mixed {
+    $value = parent::getPropValue() ?? [];
+    $value = is_scalar($value) ? [$value] : $value;
+    return array_values($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
+    $min = $this->propDefinition['minItems'] ?? 0;
+    $max = $this->propDefinition['maxItems'] ?? 1;
+    foreach (range(0, $max - 1) as $index) {
+      $form['value'][$index] = [
+        '#type' => 'select',
+        '#default_value' => $this->getSetting('value')[$index] ?? NULL,
+        '#options' => $this->getEnumOptions($this->propDefinition['items']),
+        '#title' => '#' . ($index + 1),
+        '#required' => ($index < $min),
+        '#empty_value' => "",
+      ];
+    }
+    return $form;
+  }
+
+}
diff --git a/src/PropTypePluginBase.php b/src/PropTypePluginBase.php
index a28f610a6..0a5f9582b 100644
--- a/src/PropTypePluginBase.php
+++ b/src/PropTypePluginBase.php
@@ -52,11 +52,8 @@ abstract class PropTypePluginBase extends PluginBase implements PropTypeInterfac
     if (isset($definition['description'])) {
       $summary[] = $definition['description'];
     }
-    if (isset($definition['default']) && is_scalar($definition['default'])) {
-      $summary[] = $this->t("Default: @default", ["@default" => $definition['default']]);
-    }
-    if (isset($definition['default']) && is_array($definition['default'])) {
-      $summary[] = $this->t("Default: @default", ["@default" => implode(", ", $definition['default'])]);
+    if (isset($definition['default'])) {
+      $summary[] = $this->t("Default: @default", ["@default" => json_encode($definition['default'])]);
     }
     if (isset($definition['ui_patterns']['required']) && $definition['ui_patterns']['required']) {
       $summary[] = $this->t("Required");
diff --git a/src/SchemaManager/CompatibilityChecker.php b/src/SchemaManager/CompatibilityChecker.php
index 1293d95d7..5bb53cca9 100644
--- a/src/SchemaManager/CompatibilityChecker.php
+++ b/src/SchemaManager/CompatibilityChecker.php
@@ -159,14 +159,17 @@ class CompatibilityChecker {
     if (!isset($checked_schema["items"]) && isset($reference_schema["items"])) {
       return FALSE;
     }
+    if (($reference_schema["uniqueItems"] ?? FALSE) && (!isset($checked_schema["uniqueItems"]) || !$checked_schema["uniqueItems"])) {
+      return FALSE;
+    }
     // https://json-schema.org/understanding-json-schema/reference/array#items
     if (isset($checked_schema["items"]) && isset($reference_schema["items"])) {
       if (!$this->isCompatible($checked_schema["items"], $reference_schema["items"])) {
         return FALSE;
       }
     }
-    // contains, mincontains, maxcontains, length and uniqueness are not managed
-    // yet.
+    // minItems, maxItems, contains, mincontains, maxcontains and length are
+    // not managed yet.
     return TRUE;
   }
 
-- 
GitLab