-
Notifications
You must be signed in to change notification settings - Fork 28
[AdminUi] Add badge field component with enum support #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlexandreBulete
wants to merge
2
commits into
Sylius:main
Choose a base branch
from
AlexandreBulete:feature/add-badge-field-component
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }; | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| { | ||
| 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); | ||
| } | ||
| } | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?