From 3cc62b701a5939184c124fef3a142594a4f53487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bulet=C3=A9?= Date: Fri, 14 Nov 2025 15:35:26 +0100 Subject: [PATCH 1/2] feat: add badge twig component (with Enum compatibility) --- .../shared/grid/field/badge.html.twig | 3 ++ .../shared/helper/field/badge.html.twig | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/BootstrapAdminUi/templates/shared/grid/field/badge.html.twig create mode 100644 src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig diff --git a/src/BootstrapAdminUi/templates/shared/grid/field/badge.html.twig b/src/BootstrapAdminUi/templates/shared/grid/field/badge.html.twig new file mode 100644 index 00000000..9a7268b1 --- /dev/null +++ b/src/BootstrapAdminUi/templates/shared/grid/field/badge.html.twig @@ -0,0 +1,3 @@ +{% import "@SyliusBootstrapAdminUi/shared/helper/field/badge.html.twig" as badge %} + +{{ badge.default(data, options|default({})) }} \ No newline at end of file diff --git a/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig b/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig new file mode 100644 index 00000000..a9f966c8 --- /dev/null +++ b/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig @@ -0,0 +1,53 @@ +{% macro default(data, options) %} + {% if data is not null %} + + {# Set label, color, icon to null first #} + {% set label = null %} + {% set color = null %} + {% set icon = null %} + {% if data.name is defined %} + {% set value = data.value %} + {% else %} + {% set value = data %} + {% endif %} + + {# Define label from enum if it exists or use the value directly if it's not an enum #} + {% if data.name is defined %} + {% if data.getLabel is defined %} + {% set label = data.getLabel() %} + {% else %} + {% set label = value %} + {% endif %} + {% if data.getColor is defined %} + {% set color = data.getColor() %} + {% endif %} + {% if data.getIcon is defined %} + {% set icon = data.getIcon() %} + {% endif %} + {% else %} + {% set label = value %} + {% endif %} + + {# options.vars.*[value] overrides #} + {% if options.vars.colors[value] is defined %} + {% set color = options.vars.colors[value] %} + {% endif %} + {% if options.vars.labels[value] is defined %} + {% set label = options.vars.labels[value] %} + {% endif %} + {% if options.vars.icons[value] is defined %} + {% set icon = options.vars.icons[value] %} + {% endif %} + + + {% if icon %} + {% if ':' in icon %} + {{ ux_icon(icon, {'class': 'icon icon-sm'}) }} + {% else %} + {{ icon }} + {% endif %} + {% endif %} + {{ label }} + + {% endif %} +{% endmacro %} \ No newline at end of file From f49c72de48e7d3072611808c41208a69f28efafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bulet=C3=A9?= Date: Fri, 14 Nov 2025 20:27:39 +0100 Subject: [PATCH 2/2] feat: interface, valueObject, twigExtension (reducing twig file) ; doc: badge --- .../config/services/twig/extension.php | 24 ++ src/BootstrapAdminUi/docs/badge-field.md | 265 ++++++++++++++++++ src/BootstrapAdminUi/src/Grid/BadgeData.php | 94 +++++++ .../src/Grid/BadgeableInterface.php | 38 +++ .../src/Twig/Extension/BadgeExtension.php | 40 +++ .../shared/helper/field/badge.html.twig | 65 ++--- 6 files changed, 480 insertions(+), 46 deletions(-) create mode 100644 src/BootstrapAdminUi/config/services/twig/extension.php create mode 100644 src/BootstrapAdminUi/docs/badge-field.md create mode 100644 src/BootstrapAdminUi/src/Grid/BadgeData.php create mode 100644 src/BootstrapAdminUi/src/Grid/BadgeableInterface.php create mode 100644 src/BootstrapAdminUi/src/Twig/Extension/BadgeExtension.php diff --git a/src/BootstrapAdminUi/config/services/twig/extension.php b/src/BootstrapAdminUi/config/services/twig/extension.php new file mode 100644 index 00000000..7f48f275 --- /dev/null +++ b/src/BootstrapAdminUi/config/services/twig/extension.php @@ -0,0 +1,24 @@ +services(); + + $services->set('sylius_bootstrap_admin_ui.twig.extension.badge', BadgeExtension::class) + ->tag('twig.extension'); +}; + diff --git a/src/BootstrapAdminUi/docs/badge-field.md b/src/BootstrapAdminUi/docs/badge-field.md new file mode 100644 index 00000000..e2cf4b07 --- /dev/null +++ b/src/BootstrapAdminUi/docs/badge-field.md @@ -0,0 +1,265 @@ +# Badge Field + +The badge field allows you to render colored badges with icons in your Sylius grids. + +## Basic Usage + +### Using with Enums (Recommended) + +The cleanest way to use badges is by implementing `BadgeableInterface` on your enums: + +```php + 'Active', + self::INACTIVE => 'Inactive', + }; + } + + public function getColor(): string + { + return match ($this) { + self::ACTIVE => 'success', + self::INACTIVE => 'danger', + }; + } + + public function getIcon(): ?string + { + return match ($this) { + self::ACTIVE => 'heroicons:check', + self::INACTIVE => 'heroicons:x-mark', + }; + } + + public function getValue(): string + { + return $this->value; + } +} +``` + +Then in your grid: + +```php +use Sylius\Bundle\GridBundle\Builder\Field\TwigField; + +$gridBuilder->addField( + TwigField::create('status', '@SyliusBootstrapAdminUi/shared/grid/field/badge.html.twig') + ->setLabel('Status') + ->setSortable(true) +); +``` + +### Using without Icons + +The `getIcon()` method is required by the interface, but you can return `null` if you don't want icons: + +```php + 'High Priority', + self::MEDIUM => 'Medium Priority', + self::LOW => 'Low Priority', + }; + } + + public function getColor(): string + { + return match ($this) { + self::HIGH => 'danger', + self::MEDIUM => 'warning', + self::LOW => 'info', + }; + } + + public function getIcon(): ?string + { + // No icons - just return null + return null; + } + + public function getValue(): string + { + return $this->value; + } +} +``` + +### Using with Simple Strings + +If you're displaying a simple string value: + +```php +$gridBuilder->addField( + TwigField::create('type', '@SyliusBootstrapAdminUi/shared/grid/field/badge.html.twig') + ->setLabel('Type') +); +``` + +### Using with Options Override + +You can override labels, colors, and icons using options: + +```php +$gridBuilder->addField( + TwigField::create('status', '@SyliusBootstrapAdminUi/shared/grid/field/badge.html.twig') + ->setLabel('Status') + ->withOptions([ + 'vars' => [ + 'labels' => [ + 'active' => 'Active', + 'inactive' => 'Inactive', + ], + 'colors' => [ + 'active' => 'success', + 'inactive' => 'secondary', + ], + 'icons' => [ + 'active' => 'heroicons:check-circle', + 'inactive' => 'heroicons:x-circle', + ], + ], + ]) +); +``` + +## BadgeableInterface + +The `BadgeableInterface` provides a contract for objects that can be displayed as badges: + +```php +interface BadgeableInterface +{ + /** + * Returns the human-readable label for the badge. + */ + public function getLabel(): string; + + /** + * Returns the color variant for the badge. + * Should be one of: primary, secondary, success, danger, warning, info, light, dark + */ + public function getColor(): string; + + /** + * Returns the icon to display in the badge (optional). + * Return null if you don't want an icon. + * Can be a UX Icon name (e.g., 'heroicons:check') or a simple character/emoji. + */ + public function getIcon(): ?string; + + /** + * Returns the value for test attributes and data attributes. + */ + public function getValue(): string; +} +``` + +## Colors + +Available Bootstrap color variants: +- `primary` - Blue +- `secondary` - Gray +- `success` - Green +- `danger` - Red +- `warning` - Yellow/Orange +- `info` - Light blue +- `light` - Light gray +- `dark` - Dark gray/black + +## Icons + +Icons are **optional**. The `getIcon()` method returns `?string` (nullable). + +### No Icons +Simply return `null` if you don't want icons: + +```php +public function getIcon(): ?string +{ + return null; // No icon will be displayed +} +``` + +### With Icons +Icons can be: +1. **UX Icons** (recommended): Use the format `bundle:icon-name`, e.g., `heroicons:check` +2. **Simple characters/emojis**: e.g., `✓`, `⚠`, `🔥` + +Popular UX icon bundles: +- `heroicons:` - Heroicons +- `fa:` - Font Awesome +- `bi:` - Bootstrap Icons +- `lucide:` - Lucide Icons + +### Selective Icons +You can also have icons for some cases only: + +```php +public function getIcon(): ?string +{ + return match ($this) { + self::CRITICAL => '🔥', // Icon for critical + self::ERROR => '✗', // Icon for error + default => null, // No icon for other cases + }; +} +``` + +## Architecture + +The badge system consists of: + +1. **`BadgeableInterface`**: Contract for badge-displayable objects +2. **`BadgeData`**: Value object that normalizes badge data from various sources +3. **`BadgeExtension`**: Twig extension that provides the `sylius_badge_data()` function +4. **Badge templates**: Twig macros for rendering badges + +This architecture ensures: +- **Type safety**: No magic method calls via Twig's `is defined` +- **Separation of concerns**: Logic in PHP, not Twig +- **Flexibility**: Supports enums, arrays, and strings +- **Testability**: Value objects and interfaces are easy to test + +## Testing + +Test attributes are automatically added to badges for E2E testing: + +```html + + ✓ Healthy + +``` + +The test attribute uses the `getValue()` method (for `BadgeableInterface` implementations) or the label as a fallback. + diff --git a/src/BootstrapAdminUi/src/Grid/BadgeData.php b/src/BootstrapAdminUi/src/Grid/BadgeData.php new file mode 100644 index 00000000..1d57f67f --- /dev/null +++ b/src/BootstrapAdminUi/src/Grid/BadgeData.php @@ -0,0 +1,94 @@ +getLabel(), + color: $badgeable->getColor(), + icon: $badgeable->getIcon(), + value: $badgeable->getValue(), + ); + } + + /** + * Creates BadgeData from an array with optional overrides. + * + * @param array{label?: string, color?: string, icon?: string, value?: string} $data + * @param array{labels?: array, colors?: array, icons?: array} $overrides + */ + public static function fromArray(array $data, array $overrides = []): self + { + $value = $data['value'] ?? $data['label'] ?? 'unknown'; + + return new self( + label: $overrides['labels'][$value] ?? $data['label'] ?? $value, + color: $overrides['colors'][$value] ?? $data['color'] ?? 'primary', + icon: $overrides['icons'][$value] ?? $data['icon'] ?? null, + value: $value, + ); + } + + /** + * Creates BadgeData from a simple string value. + */ + public static function fromString(string $value, array $overrides = []): self + { + return new self( + label: $overrides['labels'][$value] ?? $value, + color: $overrides['colors'][$value] ?? 'grey', + icon: $overrides['icons'][$value] ?? null, + value: $value, + ); + } + + /** + * Creates BadgeData from mixed input (auto-detection). + * + * @param BadgeableInterface|array|string|null $data + * @param array{labels?: array, colors?: array, icons?: array} $overrides + */ + public static function from(mixed $data, array $overrides = []): ?self + { + if ($data === null) { + return null; + } + + if ($data instanceof BadgeableInterface) { + return self::fromBadgeable($data); + } + + if (is_array($data)) { + return self::fromArray($data, $overrides); + } + + if (is_string($data)) { + return self::fromString($data, $overrides); + } + + // Fallback: convert to string + return self::fromString((string) $data, $overrides); + } +} + diff --git a/src/BootstrapAdminUi/src/Grid/BadgeableInterface.php b/src/BootstrapAdminUi/src/Grid/BadgeableInterface.php new file mode 100644 index 00000000..ca04570e --- /dev/null +++ b/src/BootstrapAdminUi/src/Grid/BadgeableInterface.php @@ -0,0 +1,38 @@ + $options['vars']['labels'] ?? [], + 'colors' => $options['vars']['colors'] ?? [], + 'icons' => $options['vars']['icons'] ?? [], + ]; + + return BadgeData::from($data, $overrides); + } +} + diff --git a/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig b/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig index a9f966c8..ee1a244b 100644 --- a/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig +++ b/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig @@ -1,53 +1,26 @@ +{# + Badge rendering macro for grid fields. + + Accepts: + - Objects implementing BadgeableInterface (enums with getLabel(), getColor(), getIcon(), getValue()) + - Arrays with keys: label, color, icon, value + - Simple strings + + Options can override labels, colors, and icons via options.vars.labels, options.vars.colors, options.vars.icons +#} {% macro default(data, options) %} - {% if data is not null %} - - {# Set label, color, icon to null first #} - {% set label = null %} - {% set color = null %} - {% set icon = null %} - {% if data.name is defined %} - {% set value = data.value %} - {% else %} - {% set value = data %} - {% endif %} - - {# Define label from enum if it exists or use the value directly if it's not an enum #} - {% if data.name is defined %} - {% if data.getLabel is defined %} - {% set label = data.getLabel() %} - {% else %} - {% set label = value %} - {% endif %} - {% if data.getColor is defined %} - {% set color = data.getColor() %} - {% endif %} - {% if data.getIcon is defined %} - {% set icon = data.getIcon() %} - {% endif %} - {% else %} - {% set label = value %} - {% endif %} - - {# options.vars.*[value] overrides #} - {% if options.vars.colors[value] is defined %} - {% set color = options.vars.colors[value] %} - {% endif %} - {% if options.vars.labels[value] is defined %} - {% set label = options.vars.labels[value] %} - {% endif %} - {% if options.vars.icons[value] is defined %} - {% set icon = options.vars.icons[value] %} - {% endif %} - - - {% if icon %} - {% if ':' in icon %} - {{ ux_icon(icon, {'class': 'icon icon-sm'}) }} + {% set badgeData = sylius_badge_data(data, options|default({})) %} + + {% if badgeData %} + + {% if badgeData.icon %} + {% if ':' in badgeData.icon %} + {{ ux_icon(badgeData.icon, {'class': 'icon icon-sm'}) }} {% else %} - {{ icon }} + {{ badgeData.icon }} {% endif %} {% endif %} - {{ label }} + {{ badgeData.label }} {% endif %} {% endmacro %} \ No newline at end of file