Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/BootstrapAdminUi/config/services/twig/extension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Sylius\BootstrapAdminUi\Twig\Extension\BadgeExtension;

return function (ContainerConfigurator $configurator): void {
$services = $configurator->services();

$services->set('sylius_bootstrap_admin_ui.twig.extension.badge', BadgeExtension::class)
->tag('twig.extension');
};

265 changes: 265 additions & 0 deletions src/BootstrapAdminUi/docs/badge-field.md
Original file line number Diff line number Diff line change
@@ -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
<?php

declare(strict_types=1);

namespace App\Enums;

use Sylius\BootstrapAdminUi\Grid\BadgeableInterface;

enum MyCustomStatus: string implements BadgeableInterface
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';

public function getLabel(): string
{
return match ($this) {
self::ACTIVE => '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
<?php

declare(strict_types=1);

namespace App\Enums;

use Sylius\BootstrapAdminUi\Grid\BadgeableInterface;

enum Priority: string implements BadgeableInterface
{
case HIGH = 'high';
case MEDIUM = 'medium';
case LOW = 'low';

public function getLabel(): string
{
return match ($this) {
self::HIGH => '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
<span class="badge rounded-pill text-success" data-test="badge-healthy">
✓ Healthy
</span>
```

The test attribute uses the `getValue()` method (for `BadgeableInterface` implementations) or the label as a fallback.

94 changes: 94 additions & 0 deletions src/BootstrapAdminUi/src/Grid/BadgeData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace Sylius\BootstrapAdminUi\Grid;

/**
* Value object representing badge display data.
*
* This class normalizes badge data from various sources (BadgeableInterface, arrays, strings)
* into a consistent format for rendering.
*/
final readonly class BadgeData
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a nit-pick but shoudln't it simply be called Badge?

{
public function __construct(
public string $label,
public string $color,
public ?string $icon = null,
public ?string $value = null,
) {}

/**
* Creates BadgeData from a BadgeableInterface implementation.
*/
public static function fromBadgeable(BadgeableInterface $badgeable): self
{
return new self(
label: $badgeable->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<string, string>, colors?: array<string, string>, icons?: array<string, string>} $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<string, string>, colors?: array<string, string>, icons?: array<string, string>} $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);
}
}

Loading