Skip to content

Commit 9b4ed76

Browse files
committed
feat: add colored badges and filters for tags input (#64)
- Add TagsFilter for filtering by arbitrary tag values - Add ColorResolver for consistent badge colors across components - Display tags as colored badges in table columns and infolists - Show colored active filter indicators at top of table - Add tags-input to option colors visibility in field form - Extract hasColorOptionsEnabled to AbstractTableFilter base class Closes #64
1 parent 31023de commit 9b4ed76

File tree

8 files changed

+255
-3
lines changed

8 files changed

+255
-3
lines changed

src/FieldTypeSystem/Definitions/TagsInputFieldType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Relaticle\CustomFields\Filament\Integration\Components\Forms\TagsInputComponent;
1111
use Relaticle\CustomFields\Filament\Integration\Components\Infolists\MultiChoiceEntry;
1212
use Relaticle\CustomFields\Filament\Integration\Components\Tables\Columns\MultiChoiceColumn;
13+
use Relaticle\CustomFields\Filament\Integration\Components\Tables\Filters\TagsFilter;
1314

1415
/**
1516
* ABOUTME: Field type definition for Tags Input fields
@@ -25,6 +26,9 @@ public function configure(): FieldSchema
2526
->icon('mdi-tag-multiple')
2627
->formComponent(TagsInputComponent::class)
2728
->tableColumn(MultiChoiceColumn::class)
29+
->tableFilter(TagsFilter::class)
30+
->filterable()
31+
->searchable(false)
2832
->infolistEntry(MultiChoiceEntry::class)
2933
->priority(70)
3034
->availableValidationRules([

src/Filament/Integration/Base/AbstractTableFilter.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Filament\Tables\Filters\BaseFilter;
88
use Relaticle\CustomFields\Contracts\TableFilterInterface;
9+
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
10+
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
911
use Relaticle\CustomFields\Models\CustomField;
1012

1113
/**
@@ -18,4 +20,10 @@ abstract class AbstractTableFilter implements TableFilterInterface
1820
* Create and configure a table filter.
1921
*/
2022
abstract public function make(CustomField $customField): BaseFilter;
23+
24+
protected function hasColorOptionsEnabled(CustomField $customField): bool
25+
{
26+
return FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS)
27+
&& $customField->settings->enable_option_colors;
28+
}
2129
}

src/Filament/Integration/Components/Tables/Columns/MultiChoiceColumn.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public function make(CustomField $customField): BaseColumn
2828

2929
$column
3030
->sortable(false)
31-
->searchable(false)
3231
->getStateUsing(fn (HasCustomFields $record): array => $this->valueResolver->resolve($record, $customField));
3332

3433
return $this->applyBadgeColorsIfEnabled($column, $customField);

src/Filament/Integration/Components/Tables/Filters/SelectFilter.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Relaticle\CustomFields\Filament\Integration\Components\Tables\Filters;
66

7+
use Filament\Support\Colors\Color;
8+
use Filament\Tables\Filters\Indicator;
79
use Filament\Tables\Filters\SelectFilter as FilamentSelectFilter;
810
use Illuminate\Database\Eloquent\Builder;
911
use Relaticle\CustomFields\Filament\Integration\Base\AbstractTableFilter;
@@ -16,8 +18,9 @@ public function make(CustomField $customField): FilamentSelectFilter
1618
$filter = FilamentSelectFilter::make($customField->getFieldName())
1719
->multiple()
1820
->label($customField->name)
19-
->searchable()
20-
->options($customField->options->pluck('name', 'id')->all());
21+
->searchable();
22+
23+
$filter->options($customField->options->pluck('name', 'id')->all());
2124

2225
$filter->query(
2326
fn (array $data, Builder $query): Builder => $query->when(
@@ -30,6 +33,24 @@ public function make(CustomField $customField): FilamentSelectFilter
3033
)
3134
);
3235

36+
if ($this->hasColorOptionsEnabled($customField)) {
37+
$filter->indicateUsing(function (array $data) use ($customField): array {
38+
if (empty($data['values'])) {
39+
return [];
40+
}
41+
42+
return $customField->options
43+
->whereIn('id', $data['values'])
44+
->map(function (mixed $option) use ($customField): Indicator {
45+
$hexColor = $option->settings->color ?? null;
46+
47+
return Indicator::make("{$customField->name}: {$option->name}")
48+
->color($hexColor !== null ? Color::hex($hexColor) : 'gray');
49+
})
50+
->all();
51+
});
52+
}
53+
3354
return $filter;
3455
}
3556
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\CustomFields\Filament\Integration\Components\Tables\Filters;
6+
7+
use Filament\Support\Colors\Color;
8+
use Filament\Tables\Filters\Indicator;
9+
use Filament\Tables\Filters\SelectFilter as FilamentSelectFilter;
10+
use Illuminate\Database\Eloquent\Builder;
11+
use Relaticle\CustomFields\CustomFields;
12+
use Relaticle\CustomFields\Filament\Integration\Base\AbstractTableFilter;
13+
use Relaticle\CustomFields\Models\CustomField;
14+
15+
final class TagsFilter extends AbstractTableFilter
16+
{
17+
public function make(CustomField $customField): FilamentSelectFilter
18+
{
19+
$filter = FilamentSelectFilter::make($customField->getFieldName())
20+
->multiple()
21+
->label($customField->name)
22+
->searchable();
23+
24+
$filter->options(fn (): array => $this->getExistingTags($customField));
25+
26+
$filter->query(
27+
fn (array $data, Builder $query): Builder => $query->when(
28+
! empty($data['values']),
29+
fn (Builder $query): Builder => $query->whereHas('customFieldValues', function (Builder $query) use ($customField, $data): void {
30+
$query->where('custom_field_id', $customField->id);
31+
32+
foreach ($data['values'] as $tag) {
33+
$query->whereJsonContains('json_value', $tag);
34+
}
35+
}),
36+
)
37+
);
38+
39+
$filter->indicateUsing(function (array $data) use ($customField): array {
40+
if (empty($data['values'])) {
41+
return [];
42+
}
43+
44+
$optionColors = $this->hasColorOptionsEnabled($customField)
45+
? $customField->options
46+
->filter(fn (mixed $option): bool => filled($option->settings->color ?? null))
47+
->mapWithKeys(fn (mixed $option): array => [$option->name => $option->settings->color])
48+
->all()
49+
: [];
50+
51+
/** @var array<int, string> $values */
52+
$values = $data['values'];
53+
54+
return collect($values)
55+
->map(function (string $tag) use ($customField, $optionColors): Indicator {
56+
$hexColor = $optionColors[$tag] ?? null;
57+
58+
return Indicator::make("{$customField->name}: {$tag}")
59+
->color($hexColor !== null ? Color::hex($hexColor) : 'gray');
60+
})
61+
->all();
62+
});
63+
64+
return $filter;
65+
}
66+
67+
/**
68+
* @return array<string, string>
69+
*/
70+
private function getExistingTags(CustomField $customField): array
71+
{
72+
$valueModel = CustomFields::newValueModel();
73+
74+
$allTags = $valueModel::query()
75+
->where('custom_field_id', $customField->id)
76+
->whereNotNull('json_value')
77+
->pluck('json_value')
78+
->flatten()
79+
->unique()
80+
->filter()
81+
->sort()
82+
->values();
83+
84+
return $allTags->mapWithKeys(fn (string $tag): array => [$tag => $tag])->all();
85+
}
86+
}

src/Filament/Integration/Concerns/Shared/ConfiguresBadgeColors.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ trait ConfiguresBadgeColors
1313
{
1414
protected function applyBadgeColorsIfEnabled($component, CustomField $customField)
1515
{
16+
if ($customField->typeData->acceptsArbitraryValues) {
17+
return $this->applyTagsBadgeColors($component, $customField);
18+
}
19+
1620
if (! $this->shouldApplyBadgeColors($customField)) {
1721
return $component;
1822
}
@@ -25,6 +29,26 @@ protected function applyBadgeColorsIfEnabled($component, CustomField $customFiel
2529
});
2630
}
2731

32+
/**
33+
* Apply badge styling for tags (fields with arbitrary values).
34+
* Always displays as badges with predefined option colors or gray fallback.
35+
*/
36+
private function applyTagsBadgeColors($component, CustomField $customField)
37+
{
38+
return $component->badge()
39+
->color(function ($state) use ($customField): array|string {
40+
if ($this->shouldApplyBadgeColors($customField)) {
41+
$option = $customField->options->where('name', $state)->first();
42+
43+
if ($option?->settings->color) {
44+
return Color::hex($option->settings->color);
45+
}
46+
}
47+
48+
return 'gray';
49+
});
50+
}
51+
2852
private function shouldApplyBadgeColors(CustomField $customField): bool
2953
{
3054
return FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS)

src/Filament/Management/Schemas/FieldForm.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ public static function schema(bool $withOptionsRelationship = true): array
356356
in_array((string) $get('type'), [
357357
'select',
358358
'multi_select',
359+
'tags-input',
359360
])
360361
),
361362
// Multi-value settings

src/Support/ColorResolver.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\CustomFields\Support;
6+
7+
use Filament\Support\Colors\Color;
8+
use Filament\Support\Facades\FilamentColor;
9+
use Filament\Support\View\Components\BadgeComponent;
10+
11+
/**
12+
* Resolves color values to ensure consistency across all custom field components.
13+
*
14+
* Supports:
15+
* - Semantic colors (primary, danger, gray, etc.)
16+
* - Hex colors (#ff0000)
17+
* - Color constant arrays (already full palettes)
18+
*
19+
* This ensures that badges, filters, and other components display the same
20+
* color for the same option, using Filament's color system.
21+
*/
22+
final class ColorResolver
23+
{
24+
/**
25+
* Resolve a hex color to a full Filament color palette.
26+
*
27+
* @return array<int, string>|null
28+
*/
29+
public static function hexToPalette(?string $hexColor): ?array
30+
{
31+
if (blank($hexColor)) {
32+
return null;
33+
}
34+
35+
if (! str_starts_with($hexColor, '#') || ! preg_match('/^#[0-9A-Fa-f]{6}$/', $hexColor)) {
36+
return null;
37+
}
38+
39+
try {
40+
return Color::hex($hexColor);
41+
} catch (\Exception) {
42+
return null;
43+
}
44+
}
45+
46+
/**
47+
* Get the accessible text color shade for a given color palette.
48+
* Uses Filament's BadgeComponent logic to ensure proper contrast.
49+
*
50+
* @param array<int, string> $palette
51+
*/
52+
public static function getTextShade(array $palette): int
53+
{
54+
$component = new BadgeComponent;
55+
$colorMap = $component->getColorMap($palette);
56+
57+
return $colorMap['text'];
58+
}
59+
60+
/**
61+
* Convert an OKLCH color string to RGB format.
62+
*
63+
* @param string $oklch OKLCH color string from Color palette (e.g., "oklch(0.72 0.11 178)")
64+
* @return string RGB color string (e.g., "rgb(107, 114, 128)")
65+
*/
66+
public static function oklchToRgb(string $oklch): string
67+
{
68+
return Color::convertToRgb($oklch);
69+
}
70+
71+
/**
72+
* Get badge-style colors (background and text) for a hex color.
73+
* Returns the same colors that Filament badges use for visual consistency.
74+
* Returns gray colors if hex color is null or invalid.
75+
*
76+
* @return array{background: string, text: string}
77+
*/
78+
public static function getBadgeColors(?string $hexColor): array
79+
{
80+
$palette = self::hexToPalette($hexColor);
81+
82+
if ($palette === null) {
83+
return self::getGrayBadgeColors();
84+
}
85+
86+
$textShade = self::getTextShade($palette);
87+
88+
return [
89+
'background' => self::oklchToRgb($palette[50]),
90+
'text' => self::oklchToRgb($palette[$textShade]),
91+
];
92+
}
93+
94+
/**
95+
* Get gray badge colors for arbitrary/uncolored values.
96+
*
97+
* @return array{background: string, text: string}
98+
*/
99+
public static function getGrayBadgeColors(): array
100+
{
101+
/** @var array<int, string> $grayPalette */
102+
$grayPalette = FilamentColor::getColor('gray');
103+
104+
return [
105+
'background' => self::oklchToRgb($grayPalette[100]),
106+
'text' => self::oklchToRgb($grayPalette[600]),
107+
];
108+
}
109+
}

0 commit comments

Comments
 (0)