Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
filament-admix-table-conventions
filament-admix-table-conventions
agenciafmd/filament-starter
claude-code
filament-admix
filament-tables
bulk-actions
table-filters
permissions
default-sort

Use this skill whenever creating or editing the Table class of a Filament Resource inside an admix `local-{plural}` package — columns, filters, record/bulk actions, and `defaultSort` via the `WithScopes` `sort()` scope. Do not use it for Form/Schema work (see `filament-admix-form-fields`) or for scaffolding a whole new package (see `creating-filament-admix-package`).

Install
php artisan boost:add-skill agenciafmd/filament-starter

Save this content to: .claude/skills/filament-admix-table-conventions/SKILL.md

# Table do resource admix - /src/Resources/Articles/Tables/ArticlesTable.php tabela do resource de articles a listagem

principal dos campos, quando disponíveis, são: title ou name, published_at, star e is_active os filtros principais,
quando disponíveis, são: is_active, star, tags e published_at na ação padrão de ordenação (defaultSort), utilize o scope
`sort` da trait `WithScopes` (`$query->sort()`) — ele já ordena pelos campos definidos em `$defaultSort` no Model (veja
a skill `creating-filament-admix-package`) o `BulkActionGroup`, deve conter `DeleteBulkAction::make()`,
`ForceDeleteBulkAction::make()` e `RestoreBulkAction::make()` as actions padrão (Edit, Delete, ForceDelete, Restore) e
as colunas editáveis (ToggleColumn etc.) já respeitam as permissões do Grupo do usuário; uma action própria
(`Action::make('send')`) precisa de `->authorize('send')` e da ability declarada em `getExtraPermissions()` no Resource
— veja a skill `filament-admix-permissions`. Ao chamar `->disabled()` numa coluna editável, inclua
`Gate::denies('update', $record)` na condição, pois ela sobrescreve o padrão. O formato de data das colunas vem de
`config()->string('filament-admix.timestamp.format', 'd/m/Y H:i:s')`. Os valores dos filtros (`$data`) chegam como
`mixed`: leia cada um pelo helper privado `filterValue()`, que devolve `string|null`, e tipe o valor na closure do
`when()` (`string $value`, `string $date`). O `$query->sort()` do `defaultSort` é reconhecido pelo PHPStan por uma
extensão do admix, então não precisa de anotação

<!-- Example content of ArticlesTable -->
```php
declare(strict_types=1); namespace Agenciafmd\Articles\Resources\Articles\Tables; use
    Agenciafmd\Articles\Services\ArticleService; use Filament\Actions\BulkActionGroup; use
    Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ForceDeleteBulkAction; use
    Filament\Actions\RestoreBulkAction; use Filament\Forms\Components\DateTimePicker; use
    Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Filters\Filter;
    use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\TernaryFilter; use
    Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; final
    class ArticlesTable { public static function configure(Table $table): Table { return $table ->columns([
    TextColumn::make('title') ->translateLabel() ->sortable() ->searchable(), TextColumn::make('published_at')
    ->translateLabel() ->dateTime(config()->string('filament-admix.timestamp.format', 'd/m/Y H:i:s')) ->sortable(),
    ToggleColumn::make('star')
    ->translateLabel() ->sortable(), ToggleColumn::make('is_active') ->translateLabel() ->sortable(), ]) ->filters([
    TernaryFilter::make('is_active') ->translateLabel(), TernaryFilter::make('star') ->translateLabel(),
    SelectFilter::make('tags') ->translateLabel() ->options(fn (): array => ArticleService::make() ->tags() ->all())
    ->query(fn (Builder $query, array $data): Builder => $query->when(self::filterValue($data, 'value'), fn (Builder
    $query, string $value): Builder => $query->whereJsonContains('tags', $value))), Filter::make('published_at')
    ->schema([ DateTimePicker::make('published_from') ->translateLabel(), DateTimePicker::make('published_until')
    ->translateLabel(), ]) ->query(fn (Builder $query, array $data): Builder => $query ->when(
    self::filterValue($data, 'published_from'), fn (Builder $query, string $date): Builder =>
    $query->whereDate('published_at', '>=', $date), ) ->when( self::filterValue($data, 'published_until'), fn (Builder
    $query, string $date): Builder => $query->whereDate('published_at', '<=', $date), )), TrashedFilter::make(), ])
    ->recordActions([ EditAction::make(), ]) ->toolbarActions([ BulkActionGroup::make([ DeleteBulkAction::make(),
    ForceDeleteBulkAction::make(), RestoreBulkAction::make(), ]), ]) ->defaultSort(fn (Builder $query): Builder =>
    $query->sort()); } /** @param array<array-key, mixed> $data */ private static function filterValue(array $data,
    string $key): ?string { $value = $data[$key] ?? null; return is_string($value) && $value !== '' ? $value : null; } }
```

# RelationManager - /src/Resources/Articles/RelationManagers/CommentsRelationManager.php um RelationManager é uma

tabela: as convenções acima valem todas aqui, inclusive o `defaultSort` com o scope `sort()`. o que muda: - o nome da
classe é o nome do relacionamento + `RelationManager` (`comments()` gera `CommentsRelationManager`, `favoritedBy()` gera
`FavoritedByRelationManager`) e é ele que vai em `protected static string $relationship` - o arquivo vive no pacote do
**model que a tabela lista**, não no pacote do resource que registra o RelationManager. Numa relação entre dois pacotes,
isso significa que cada lado hospeda o RelationManager do outro: `local-franchisees` tem o `FavoritedByRelationManager`
(lista franqueados, registrado no `RecipeResource`) e `local-recipes` tem o `FavoriteRecipesRelationManager` (lista
gelatos, registrado no `FranchiseeResource`) - o título vem de `getTitle()` com `__()`, não do nome do relacionamento -
o registro é em `getRelations()` do resource, sempre antes do `AuditsRelationManager::class` ## RelationManager só
leitura quando o vínculo é criado fora do painel (pelo app, por exemplo), o RelationManager é só leitura. duas camadas,
porque uma só não basta: - nenhuma action registrada — sem `headerActions`, `recordActions` nem `toolbarActions` -
`isReadOnly(): true` com o atributo `#[Override]`, que faz o Filament negar
attach/detach/associate/dissociate/create/edit/delete mesmo se alguém adicionar a action mais tarde use
`IconColumn::make('is_active')->boolean()` em vez de `ToggleColumn` — o toggle grava no registro. coluna de campo do
pivot (`created_at` do `withTimestamps()`, por exemplo) precisa de state explícito, porque o pivot não é atributo do
model listado.

<!-- Example content of FavoritedByRelationManager -->
```php
declare(strict_types=1); namespace Agenciafmd\Franchisees\Resources\Franchisees\RelationManagers; use
    Agenciafmd\Franchisees\Models\Franchisee; use Carbon\CarbonInterface; use
    Filament\Resources\RelationManagers\RelationManager; use Filament\Tables\Columns\IconColumn; use
    Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use
    Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; use Override; final class
    FavoritedByRelationManager extends RelationManager { protected static string $relationship = 'favoritedBy'; public
    static function getTitle(Model $ownerRecord, string $pageClass): string { return __('Favorites'); } #[Override]
    public function isReadOnly(): bool { return true; } public function table(Table $table): Table { return $table
    ->columns([ TextColumn::make('name') ->translateLabel() ->sortable() ->searchable(),
    TextColumn::make('favorited_at') ->label(__('Favorited at')) ->state(function (Franchisee $record): ?CarbonInterface
    { $pivot = $record->getRelation('pivot'); return $pivot instanceof Pivot ? $pivot->created_at : null; })
    ->dateTime(config()->string('filament-admix.timestamp.format', 'd/m/Y H:i:s')), IconColumn::make('is_active')
    ->translateLabel() ->boolean()
    ->sortable(), ]) ->defaultSort(fn (Builder $query): Builder => $query->sort()); } }
```

Table do resource admix - /src/Resources/Articles/Tables/ArticlesTable.php tabela do resource de articles a listagem

principal dos campos, quando disponíveis, são: title ou name, published_at, star e is_active os filtros principais, quando disponíveis, são: is_active, star, tags e published_at na ação padrão de ordenação (defaultSort), utilize o scope sort da trait WithScopes ($query->sort()) — ele já ordena pelos campos definidos em $defaultSort no Model (veja a skill creating-filament-admix-package) o BulkActionGroup, deve conter DeleteBulkAction::make(), ForceDeleteBulkAction::make() e RestoreBulkAction::make() as actions padrão (Edit, Delete, ForceDelete, Restore) e as colunas editáveis (ToggleColumn etc.) já respeitam as permissões do Grupo do usuário; uma action própria (Action::make('send')) precisa de ->authorize('send') e da ability declarada em getExtraPermissions() no Resource — veja a skill filament-admix-permissions. Ao chamar ->disabled() numa coluna editável, inclua Gate::denies('update', $record) na condição, pois ela sobrescreve o padrão. O formato de data das colunas vem de config()->string('filament-admix.timestamp.format', 'd/m/Y H:i:s'). Os valores dos filtros ($data) chegam como mixed: leia cada um pelo helper privado filterValue(), que devolve string|null, e tipe o valor na closure do when() (string $value, string $date). O $query->sort() do defaultSort é reconhecido pelo PHPStan por uma extensão do admix, então não precisa de anotação

declare(strict_types=1); namespace Agenciafmd\Articles\Resources\Articles\Tables; use
    Agenciafmd\Articles\Services\ArticleService; use Filament\Actions\BulkActionGroup; use
    Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ForceDeleteBulkAction; use
    Filament\Actions\RestoreBulkAction; use Filament\Forms\Components\DateTimePicker; use
    Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Filters\Filter;
    use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\TernaryFilter; use
    Filament\Tables\Filters\TrashedFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; final
    class ArticlesTable { public static function configure(Table $table): Table { return $table ->columns([
    TextColumn::make('title') ->translateLabel() ->sortable() ->searchable(), TextColumn::make('published_at')
    ->translateLabel() ->dateTime(config()->string('filament-admix.timestamp.format', 'd/m/Y H:i:s')) ->sortable(),
    ToggleColumn::make('star')
    ->translateLabel() ->sortable(), ToggleColumn::make('is_active') ->translateLabel() ->sortable(), ]) ->filters([
    TernaryFilter::make('is_active') ->translateLabel(), TernaryFilter::make('star') ->translateLabel(),
    SelectFilter::make('tags') ->translateLabel() ->options(fn (): array => ArticleService::make() ->tags() ->all())
    ->query(fn (Builder $query, array $data): Builder => $query->when(self::filterValue($data, 'value'), fn (Builder
    $query, string $value): Builder => $query->whereJsonContains('tags', $value))), Filter::make('published_at')
    ->schema([ DateTimePicker::make('published_from') ->translateLabel(), DateTimePicker::make('published_until')
    ->translateLabel(), ]) ->query(fn (Builder $query, array $data): Builder => $query ->when(
    self::filterValue($data, 'published_from'), fn (Builder $query, string $date): Builder =>
    $query->whereDate('published_at', '>=', $date), ) ->when( self::filterValue($data, 'published_until'), fn (Builder
    $query, string $date): Builder => $query->whereDate('published_at', '<=', $date), )), TrashedFilter::make(), ])
    ->recordActions([ EditAction::make(), ]) ->toolbarActions([ BulkActionGroup::make([ DeleteBulkAction::make(),
    ForceDeleteBulkAction::make(), RestoreBulkAction::make(), ]), ]) ->defaultSort(fn (Builder $query): Builder =>
    $query->sort()); } /** @param array<array-key, mixed> $data */ private static function filterValue(array $data,
    string $key): ?string { $value = $data[$key] ?? null; return is_string($value) && $value !== '' ? $value : null; } }

RelationManager - /src/Resources/Articles/RelationManagers/CommentsRelationManager.php um RelationManager é uma

tabela: as convenções acima valem todas aqui, inclusive o defaultSort com o scope sort(). o que muda: - o nome da classe é o nome do relacionamento + RelationManager (comments() gera CommentsRelationManager, favoritedBy() gera FavoritedByRelationManager) e é ele que vai em protected static string $relationship - o arquivo vive no pacote do model que a tabela lista, não no pacote do resource que registra o RelationManager. Numa relação entre dois pacotes, isso significa que cada lado hospeda o RelationManager do outro: local-franchisees tem o FavoritedByRelationManager (lista franqueados, registrado no RecipeResource) e local-recipes tem o FavoriteRecipesRelationManager (lista gelatos, registrado no FranchiseeResource) - o título vem de getTitle() com __(), não do nome do relacionamento - o registro é em getRelations() do resource, sempre antes do AuditsRelationManager::class ## RelationManager só leitura quando o vínculo é criado fora do painel (pelo app, por exemplo), o RelationManager é só leitura. duas camadas, porque uma só não basta: - nenhuma action registrada — sem headerActions, recordActions nem toolbarActions - isReadOnly(): true com o atributo #[Override], que faz o Filament negar attach/detach/associate/dissociate/create/edit/delete mesmo se alguém adicionar a action mais tarde use IconColumn::make('is_active')->boolean() em vez de ToggleColumn — o toggle grava no registro. coluna de campo do pivot (created_at do withTimestamps(), por exemplo) precisa de state explícito, porque o pivot não é atributo do model listado.

declare(strict_types=1); namespace Agenciafmd\Franchisees\Resources\Franchisees\RelationManagers; use
    Agenciafmd\Franchisees\Models\Franchisee; use Carbon\CarbonInterface; use
    Filament\Resources\RelationManagers\RelationManager; use Filament\Tables\Columns\IconColumn; use
    Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use
    Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; use Override; final class
    FavoritedByRelationManager extends RelationManager { protected static string $relationship = 'favoritedBy'; public
    static function getTitle(Model $ownerRecord, string $pageClass): string { return __('Favorites'); } #[Override]
    public function isReadOnly(): bool { return true; } public function table(Table $table): Table { return $table
    ->columns([ TextColumn::make('name') ->translateLabel() ->sortable() ->searchable(),
    TextColumn::make('favorited_at') ->label(__('Favorited at')) ->state(function (Franchisee $record): ?CarbonInterface
    { $pivot = $record->getRelation('pivot'); return $pivot instanceof Pivot ? $pivot->created_at : null; })
    ->dateTime(config()->string('filament-admix.timestamp.format', 'd/m/Y H:i:s')), IconColumn::make('is_active')
    ->translateLabel() ->boolean()
    ->sortable(), ]) ->defaultSort(fn (Builder $query): Builder => $query->sort()); } }
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log