Accept the option picked in selects of translatable records
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / test-postgres (push) Has been cancelled
Tests / mcp (push) Has been cancelled

Saving a page with a category failed with "El campo categoría no está
en la lista de valores permitidos" — for every category, not only the
one being tried. Filament builds such a list through a query, which
handles `->relationship('category', 'name->es')`, but validates a choice
by reading the title back off the record as a property,
`$record->{'name->es'}`, which does not exist. No label, so every
option was refused.

The same pattern was in seven more places: a call's, a testimonial's,
a talk's and a repository's project, a testimonial's service, and the
page and application filters. TranslatedOptionLabel gives Filament the
record's own translation instead, in the language of whoever uses the
panel; the JSON path stays for search and ordering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:21:43 +02:00
parent 82ef2b38dd
commit 3daa40570b
9 changed files with 138 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ namespace App\Filament\Resources\Applications\Tables;
use App\Enums\ApplicationKind;
use App\Enums\ApplicationStatus;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Application;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -93,6 +94,7 @@ class ApplicationsTable
SelectFilter::make('call')
->label(__('panel.applications.fields.call'))
->relationship('call', 'title->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title'))
->searchable()
->preload(),

View File

@@ -12,6 +12,7 @@ use App\Filament\Resources\Calls\Pages\ListCalls;
use App\Filament\Resources\Calls\RelationManagers\ApplicationsRelationManager;
use App\Filament\Support\PreviewAction;
use App\Filament\Support\TranslatableTabs;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Call;
use App\Rules\UniqueTranslatedSlug;
use BackedEnum;
@@ -141,6 +142,7 @@ class CallResource extends Resource
Select::make('project_id')
->label(__('panel.calls.fields.project'))
->relationship('project', 'title->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title'))
->searchable()
->preload()
->helperText(__('panel.calls.fields.project_hint')),

View File

@@ -7,6 +7,7 @@ namespace App\Filament\Resources\Pages\Schemas;
use App\Enums\PageStatus;
use App\Filament\Resources\Pages\PageResource;
use App\Filament\Support\TranslatableTabs;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Page;
use App\Rules\UniqueTranslatedSlug;
use App\Support\DuplicateTitles;
@@ -112,6 +113,7 @@ class PageForm
Select::make('category_id')
->label(__('panel.pages.fields.category'))
->relationship('category', 'name->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('name'))
->searchable()
->preload(),

View File

@@ -8,6 +8,7 @@ use App\Enums\PageStatus;
use App\Filament\Support\PreviewAction;
use App\Filament\Support\ReviewActions;
use App\Filament\Support\ReviewStatusColumn;
use App\Filament\Support\TranslatedOptionLabel;
use App\Filament\Support\TranslationStatusColumn;
use App\Filament\Support\ViewsColumn;
use App\Models\Page;
@@ -127,6 +128,7 @@ class PagesTable
SelectFilter::make('category')
->label(__('panel.pages.fields.category'))
->relationship('category', 'name->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('name'))
->searchable()
->preload(),

View File

@@ -9,6 +9,7 @@ use App\Filament\Resources\Repositories\Pages\CreateRepository;
use App\Filament\Resources\Repositories\Pages\EditRepository;
use App\Filament\Resources\Repositories\Pages\ListRepositories;
use App\Filament\Resources\Repositories\Tables\RepositoriesTable;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Repository;
use BackedEnum;
use Filament\Forms\Components\Select;
@@ -81,6 +82,7 @@ class RepositoryResource extends Resource
Select::make('project_id')
->label(__('panel.repositories.fields.project'))
->relationship('project', 'title->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title'))
->searchable()
->preload(),

View File

@@ -9,6 +9,7 @@ use App\Filament\Resources\Talks\Pages\CreateTalk;
use App\Filament\Resources\Talks\Pages\EditTalk;
use App\Filament\Resources\Talks\Pages\ListTalks;
use App\Filament\Support\TranslatableTabs;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Talk;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
@@ -126,6 +127,7 @@ class TalkResource extends Resource
Select::make('project_id')
->label(__('panel.talks.fields.project'))
->relationship('project', 'title->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title'))
->searchable()
->preload(),

View File

@@ -9,6 +9,7 @@ use App\Filament\Resources\Testimonials\Pages\CreateTestimonial;
use App\Filament\Resources\Testimonials\Pages\EditTestimonial;
use App\Filament\Resources\Testimonials\Pages\ListTestimonials;
use App\Filament\Support\TranslatableTabs;
use App\Filament\Support\TranslatedOptionLabel;
use App\Models\Testimonial;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
@@ -137,12 +138,14 @@ class TestimonialResource extends Resource
Select::make('project_id')
->label(__('panel.testimonials.fields.project'))
->relationship('project', 'title->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title'))
->searchable()
->preload(),
Select::make('service_id')
->label(__('panel.testimonials.fields.service'))
->relationship('service', 'title->'.config('locales.default'))
->getOptionLabelFromRecordUsing(TranslatedOptionLabel::using('title'))
->searchable()
->preload(),

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Filament\Support;
use Closure;
use Illuminate\Database\Eloquent\Model;
/**
* The label of a relationship option whose title is translatable.
*
* `->relationship('category', 'name->es')` builds the list fine — that path
* goes through a query — but to validate a choice Filament reads the title
* back off the record as a property, `$record->{'name->es'}`, which does not
* exist. Every choice then came back without a label and was refused as "not
* in the list of allowed values", whichever one it was. Given this, Filament
* asks the record instead, in the language of whoever is using the panel.
*
* The JSON path stays in `relationship()`: it is still what the search box
* queries and the list is ordered by.
*/
final class TranslatedOptionLabel
{
public static function using(string $attribute): Closure
{
return fn (Model $record): ?string => $record->getTranslation($attribute, app()->getLocale());
}
}