diff --git a/src/BootstrapAdminUi/config/services/twig/extension.php b/src/BootstrapAdminUi/config/services/twig/extension.php new file mode 100644 index 000000000..7f48f2759 --- /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 000000000..e2cf4b076 --- /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 000000000..1d57f67f2 --- /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 000000000..ca04570ec --- /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/grid/field/badge.html.twig b/src/BootstrapAdminUi/templates/shared/grid/field/badge.html.twig new file mode 100644 index 000000000..9a7268b1a --- /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 000000000..ee1a244b0 --- /dev/null +++ b/src/BootstrapAdminUi/templates/shared/helper/field/badge.html.twig @@ -0,0 +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) %} + {% 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 %} + {{ badgeData.icon }} + {% endif %} + {% endif %} + {{ badgeData.label }} + + {% endif %} +{% endmacro %} \ No newline at end of file