Merge branch '3.x' into upg3

This commit is contained in:
Dan Harrin
2023-07-06 22:09:12 +01:00
82 changed files with 814 additions and 396 deletions
+1
View File
@@ -34,6 +34,7 @@
},
"autoload": {
"files": [
"packages/panels/src/global_helpers.php",
"packages/panels/src/helpers.php",
"packages/support/src/helpers.php"
],
Generated
+16 -16
View File
@@ -132,16 +132,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.275.0",
"version": "3.275.4",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "a0dfb094d3f94da2ebc86ed9583e14fdc9000f06"
"reference": "bacab9b131dc790ed0084b63f5f4889a81eee311"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0dfb094d3f94da2ebc86ed9583e14fdc9000f06",
"reference": "a0dfb094d3f94da2ebc86ed9583e14fdc9000f06",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bacab9b131dc790ed0084b63f5f4889a81eee311",
"reference": "bacab9b131dc790ed0084b63f5f4889a81eee311",
"shasum": ""
},
"require": {
@@ -221,9 +221,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.275.0"
"source": "https://github.com/aws/aws-sdk-php/tree/3.275.4"
},
"time": "2023-06-29T19:21:55+00:00"
"time": "2023-07-06T18:22:51+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@@ -4043,16 +4043,16 @@
},
{
"name": "nesbot/carbon",
"version": "2.67.0",
"version": "2.68.1",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8"
"reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/c1001b3bc75039b07f38a79db5237c4c529e04c8",
"reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da",
"reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da",
"shasum": ""
},
"require": {
@@ -4141,7 +4141,7 @@
"type": "tidelift"
}
],
"time": "2023-05-25T22:09:47+00:00"
"time": "2023-06-20T18:29:04+00:00"
},
{
"name": "nette/schema",
@@ -5665,16 +5665,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.10.21",
"version": "1.10.25",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5"
"reference": "578f4e70d117f9a90699324c555922800ac38d8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5",
"reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/578f4e70d117f9a90699324c555922800ac38d8c",
"reference": "578f4e70d117f9a90699324c555922800ac38d8c",
"shasum": ""
},
"require": {
@@ -5723,7 +5723,7 @@
"type": "tidelift"
}
],
"time": "2023-06-21T20:07:58+00:00"
"time": "2023-07-06T12:11:37+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -26,6 +26,9 @@ use Filament\Tables\Columns\Layout\Panel;
use Filament\Tables\Columns\Layout\Split;
use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\Summarizers\Count;
use Filament\Tables\Columns\Summarizers\Range;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Columns\ToggleColumn;
@@ -1092,7 +1095,177 @@ class TablesDemo extends Component implements HasForms, HasTable
->defaultPaginationPageOption(6);
}
public function postsTable(Table $table): Table
public function summaries(Table $table): Table
{
return $this->postsTable($table)
->columns([
TextColumn::make('title')
->searchable()
->sortable(),
TextColumn::make('slug'),
TextColumn::make('rating')
->numeric()
->summarize([
Average::make(),
Range::make(),
]),
TextColumn::make('status')
->badge()
->icon(fn (string $state): string => match ($state) {
'draft' => 'heroicon-o-pencil',
'reviewing' => 'heroicon-o-clock',
'published' => 'heroicon-o-check-circle',
})
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'reviewing' => 'warning',
'published' => 'success',
}),
IconColumn::make('is_featured')
->boolean()
->summarize([
Count::make()
->query(fn ($query) => $query->where('is_featured', true)),
]),
]);
}
public function grouping(Table $table): Table
{
return $this->postsTable($table, hasSeededPosts: false)
->columns([
TextColumn::make('title')
->searchable()
->sortable(),
TextColumn::make('slug'),
TextColumn::make('author.name')
->numeric(),
TextColumn::make('status')
->badge()
->icon(fn (string $state): string => match ($state) {
'draft' => 'heroicon-o-pencil',
'reviewing' => 'heroicon-o-clock',
'published' => 'heroicon-o-check-circle',
})
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'reviewing' => 'warning',
'published' => 'success',
}),
])
->defaultGroup('status');
}
public function groupingDescriptions(Table $table): Table
{
return $this->postsTable($table, hasSeededPosts: false)
->columns([
TextColumn::make('title')
->searchable()
->sortable(),
TextColumn::make('slug'),
TextColumn::make('author.name')
->numeric(),
TextColumn::make('status')
->badge()
->icon(fn (string $state): string => match ($state) {
'draft' => 'heroicon-o-pencil',
'reviewing' => 'heroicon-o-clock',
'published' => 'heroicon-o-check-circle',
})
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'reviewing' => 'warning',
'published' => 'success',
}),
])
->defaultGroup(Group::make('status')->getDescriptionFromRecordUsing(fn (Post $post): string => match ($post->status) {
'draft' => 'Posts that are still being written.',
'reviewing' => 'Posts that are being checked by the content team.',
'published' => 'Posts that are public on the website.',
}));
}
public function emptyState(Table $table): Table
{
Post::truncate();
return $table
->query(Post::query());
}
public function emptyStateHeading(Table $table): Table
{
return $this->emptyState($table)
->emptyStateHeading('No posts yet');
}
public function emptyStateDescription(Table $table): Table
{
return $this->emptyStateHeading($table)
->emptyStateDescription('Once you write your first post, it will appear here.');
}
public function emptyStateIcon(Table $table): Table
{
return $this->emptyStateDescription($table)
->emptyStateIcon('heroicon-o-bookmark');
}
public function emptyStateActions(Table $table): Table
{
return $this->emptyStateIcon($table)
->emptyStateActions([
Action::make('create')
->label('Create post')
->icon('heroicon-m-plus')
->button(),
]);
}
public function reordering(Table $table): Table
{
return $this->postsTable($table)
->columns([
TextColumn::make('title')
->searchable()
->sortable(),
TextColumn::make('slug'),
TextColumn::make('author.name')
->numeric(),
TextColumn::make('status')
->badge()
->icon(fn (string $state): string => match ($state) {
'draft' => 'heroicon-o-pencil',
'reviewing' => 'heroicon-o-clock',
'published' => 'heroicon-o-check-circle',
})
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'reviewing' => 'warning',
'published' => 'success',
}),
])
->reorderable('rating');
}
public function reorderingCustomTriggerAction(Table $table): Table
{
return $this->reordering($table)
->reorderRecordsTriggerAction(
fn (Action $action, bool $isReordering) => $action
->button()
->label($isReordering ? 'Disable reordering' : 'Enable reordering'),
);
}
public function striped(Table $table): Table
{
return $this->example($table)
->striped();
}
public function postsTable(Table $table, bool $hasSeededPosts = true): Table
{
User::truncate();
Post::truncate();
@@ -1104,6 +1277,7 @@ class TablesDemo extends Component implements HasForms, HasTable
'is_featured' => true,
'status' => 'published',
'author_id' => User::factory()->create(['name' => 'Dan Harrin'])->id,
'rating' => 8.1,
],
[
'title' => 'Top 5 best features of Filament',
@@ -1112,6 +1286,7 @@ class TablesDemo extends Component implements HasForms, HasTable
'is_featured' => false,
'status' => 'reviewing',
'author_id' => User::factory()->create(['name' => 'Ryan Chandler'])->id,
'rating' => 9.3,
],
[
'title' => 'Tips for building a great Filament plugin',
@@ -1120,6 +1295,7 @@ class TablesDemo extends Component implements HasForms, HasTable
'is_featured' => true,
'status' => 'draft',
'author_id' => User::factory()->create(['name' => 'Zep Fietje'])->id,
'rating' => 9.7,
],
[
'title' => 'Customizing Filament\'s UI with a theme',
@@ -1128,6 +1304,7 @@ class TablesDemo extends Component implements HasForms, HasTable
'is_featured' => false,
'status' => 'reviewing',
'author_id' => User::factory()->create(['name' => 'Dennis Koch'])->id,
'rating' => 9.5,
],
[
'title' => 'New Filament plugins in August',
@@ -1136,9 +1313,13 @@ class TablesDemo extends Component implements HasForms, HasTable
'is_featured' => false,
'status' => 'published',
'author_id' => User::factory()->create(['name' => 'Adam Weston'])->id,
'rating' => 8.4,
],
]);
Post::factory()->count(45)->create();
if ($hasSeededPosts) {
Post::factory()->count(45)->create();
}
return $table
->query(Post::query())
Binary file not shown.
@@ -24,6 +24,7 @@ class PostFactory extends Factory
'status' => $this->faker->randomElement(['draft', 'reviewing', 'published']),
'is_featured' => $this->faker->boolean(),
'author_id' => User::factory(),
'rating' => $this->faker->numberBetween(1, 10),
];
}
}
@@ -19,6 +19,7 @@ return new class extends Migration
$table->string('status')->nullable();
$table->text('description')->nullable();
$table->string('title')->nullable();
$table->integer('rating')->nullable();
$table->timestamps();
});
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

+113 -4
View File
@@ -1508,7 +1508,7 @@ export default {
selector: 'body',
viewport: {
width: 420,
height: 155,
height: 90,
deviceScaleFactor: 3,
},
},
@@ -1517,7 +1517,7 @@ export default {
selector: 'body',
viewport: {
width: 420,
height: 155,
height: 90,
deviceScaleFactor: 3,
},
},
@@ -1535,7 +1535,7 @@ export default {
selector: 'body',
viewport: {
width: 420,
height: 155,
height: 90,
deviceScaleFactor: 3,
},
},
@@ -1544,7 +1544,7 @@ export default {
selector: 'body',
viewport: {
width: 420,
height: 155,
height: 115,
deviceScaleFactor: 3,
},
},
@@ -2420,4 +2420,113 @@ export default {
return image.extract({ width: 1920, height: 1200, left: 0, top: 0 })
},
},
'tables/summaries': {
url: 'tables?table=summaries',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/grouping': {
url: 'tables?table=grouping',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/grouping-descriptions': {
url: 'tables?table=groupingDescriptions',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/empty-state': {
url: 'tables?table=emptyState',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/empty-state-heading': {
url: 'tables?table=emptyStateHeading',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/empty-state-description': {
url: 'tables?table=emptyStateDescription',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/empty-state-icon': {
url: 'tables?table=emptyStateIcon',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/empty-state-actions': {
url: 'tables?table=emptyStateActions',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
'tables/reordering': {
url: 'tables?table=reordering',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
before: async (page) => {
await page.click('.filament-icon-button')
await new Promise((resolve) => setTimeout(resolve, 500))
},
},
'tables/reordering/custom-trigger-action': {
url: 'tables?table=reorderingCustomTriggerAction',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
before: async (page) => {
await page.click('.filament-button')
await new Promise((resolve) => setTimeout(resolve, 500))
},
},
'tables/striped': {
url: 'tables?table=striped',
selector: 'body',
viewport: {
width: 1080,
height: 640,
deviceScaleFactor: 3,
},
},
}
+1 -1
View File
@@ -15,7 +15,7 @@ namespace Livewire\Features\SupportUnitTesting\Tests {
public function assertFormExists(string $name = 'form'): static {}
public function assertFormFieldExists(string $fieldName, string | Closure $formName = 'form', ?Closure $callback = null): static {}
public function assertFormFieldExists(string $fieldName, string | Closure $formName = 'form', ?Closure $checkFieldUsing = null): static {}
public function assertFormFieldIsDisabled(string $fieldName, string $formName = 'form'): static {}
File diff suppressed because one or more lines are too long
+17 -17
View File
@@ -106,10 +106,10 @@ You may employ the `relationship()` method of the `Select` to configure a `Belon
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
```
The `multiple()` method may be used in combination with `relationship()` to use a `BelongsToMany` relationship. Filament will load the options from the relationship, and save them back to the relationship's pivot table when the form is submitted. If a `relationshipName` is not provided, Filament will use the field name as the relationship name:
The `multiple()` method may be used in combination with `relationship()` to use a `BelongsToMany` relationship. Filament will load the options from the relationship, and save them back to the relationship's pivot table when the form is submitted. If a `name` is not provided, Filament will use the field name as the relationship name:
```php
use Filament\Forms\Components\Select;
@@ -127,7 +127,7 @@ By default, if the select is also searchable, Filament will return search result
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable(['name', 'email'])
```
@@ -139,7 +139,7 @@ If you'd like to populate the searchable options from the database when the page
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable()
->preload()
```
@@ -154,9 +154,9 @@ use Illuminate\Database\Eloquent\Builder;
Select::make('author_id')
->relationship(
relationshipName: 'author',
name: 'author',
titleAttribute: 'name',
modifyOptionsQueryUsing: fn (Builder $query) => $query->withTrashed(),
modifyQueryUsing: fn (Builder $query) => $query->withTrashed(),
)
```
@@ -172,7 +172,7 @@ $table->string('full_name')->virtualAs('concat(first_name, \' \', last_name)');
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'full_name')
->relationship(name: 'author', titleAttribute: 'full_name')
```
Alternatively, you can use the `getOptionLabelFromRecordUsing()` method to transform an option's Eloquent model into a label:
@@ -184,8 +184,8 @@ use Illuminate\Database\Eloquent\Model;
Select::make('author_id')
->relationship(
relationshipName: 'author',
modifyOptionsQueryUsing: fn (Builder $query) => $query->orderBy('first_name')->orderBy('last_name'),
name: 'author',
modifyQueryUsing: fn (Builder $query) => $query->orderBy('first_name')->orderBy('last_name'),
)
->getOptionLabelFromRecordUsing(fn (Model $record) => "{$record->first_name} {$record->last_name}")
->searchable(['first_name', 'last_name'])
@@ -200,7 +200,7 @@ use Filament\Forms\Components\Select;
use Illuminate\Database\Eloquent\Model;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->createOptionForm([
Forms\Components\TextInput::make('name')
->required(),
@@ -225,7 +225,7 @@ use Filament\Forms\Components\Select;
use Illuminate\Database\Eloquent\Model;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->editOptionAction([
Forms\Components\TextInput::make('name')
->required(),
@@ -401,7 +401,7 @@ When you're using a searchable select or multi-select, you may want to display a
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable()
->loadingMessage('Loading authors...')
```
@@ -414,7 +414,7 @@ When you're using a searchable select or multi-select, you may want to display a
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable()
->noSearchResultsMessage('No authors found.')
```
@@ -427,7 +427,7 @@ When you're using a searchable select or multi-select, you may want to display a
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable(['name', 'email'])
->searchPrompt('Search authors by their name or email address')
```
@@ -440,7 +440,7 @@ When you're using a searchable select or multi-select, you may want to display a
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable()
->searchingMessage('Searching authors...')
```
@@ -453,7 +453,7 @@ By default, Filament will wait 1000 milliseconds (1 second) before searching for
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable()
->searchDebounce(500)
```
@@ -468,7 +468,7 @@ You can limit the number of options that are displayed in a searchable select or
use Filament\Forms\Components\Select;
Select::make('author_id')
->relationship(relationshipName: 'author', titleAttribute: 'name')
->relationship(name: 'author', titleAttribute: 'name')
->searchable()
->optionsLimit(20)
```
@@ -127,7 +127,7 @@ use Illuminate\Database\Eloquent\Builder;
CheckboxList::make('technologies')
->relationship(
titleAttribute: 'name',
modifyOptionsQueryUsing: fn (Builder $query) => $query->withTrashed(),
modifyQueryUsing: fn (Builder $query) => $query->withTrashed(),
)
```
@@ -155,7 +155,7 @@ use Illuminate\Database\Eloquent\Model;
CheckboxList::make('authors')
->relationship(
modifyOptionsQueryUsing: fn (Builder $query) => $query->orderBy('first_name')->orderBy('last_name'),
modifyQueryUsing: fn (Builder $query) => $query->orderBy('first_name')->orderBy('last_name'),
)
->getOptionLabelFromRecordUsing(fn (Model $record) => "{$record->first_name} {$record->last_name}")
```
+2 -2
View File
@@ -191,7 +191,7 @@ You can further customize the rule by passing a [closure](advanced#closure-custo
use Illuminate\Validation\Rules\Exists;
Field::make('invitation')
->exists(callback: function (Exists $rule) {
->exists(modifyRuleUsing: function (Exists $rule) {
return $rule->where('is_active', 1);
})
```
@@ -436,7 +436,7 @@ You can further customize the rule by passing a [closure](advanced#closure-custo
use Illuminate\Validation\Rules\Unique;
Field::make('email')
->unique(callback: function (Unique $rule) {
->unique(modifyRuleUsing: function (Unique $rule) {
return $rule->where('is_active', 1);
})
```
@@ -50,8 +50,8 @@
x-sortable
:wire:end.stop="'mountFormComponentAction(\'' . $statePath . '\', \'reorder\', { items: $event.target.sortable.toArray() })'"
@class([
'space-y-12' => $addAction && $reorderAction,
'space-y-6' => ! ($addAction && $reorderAction),
'space-y-12' => $addAction->isVisible() && $reorderAction->isVisible(),
'space-y-6' => ! ($addAction->isVisible() && $reorderAction->isVisible()),
])
>
@php
@@ -98,7 +98,7 @@
"
class="filament-forms-builder-component-item relative rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-800 dark:ring-white/20"
>
@if ($reorderAction || $hasBlockLabels || $deleteAction || $isCollapsible || $isCloneable)
@if ($reorderAction->isVisible() || $hasBlockLabels || $cloneAction->isVisible() || $deleteAction->isVisible() || $isCollapsible || $isCloneable)
<header
@if ($isCollapsible) x-on:click.stop="isCollapsed = ! isCollapsed" @endif
@class([
@@ -106,7 +106,7 @@
'cursor-pointer' => $isCollapsible,
])
>
@if ($reorderAction)
@if ($reorderAction->isVisible())
<div x-sortable-handle>
{{ $reorderAction }}
</div>
@@ -159,7 +159,7 @@
@endif
@endif
@if ($cloneAction)
@if ($cloneAction->isVisible())
<li
class="flex items-center justify-center"
>
@@ -167,7 +167,7 @@
</li>
@endif
@if ($deleteAction)
@if ($deleteAction->isVisible())
<li
class="flex items-center justify-center"
>
@@ -212,7 +212,7 @@
{{ __('filament-forms::components.builder.collapsed') }}
</div>
@if ((! $loop->last) && $addBetweenAction && $reorderAction)
@if ((! $loop->last) && $addBetweenAction->isVisible() && $reorderAction->isVisible())
<div
x-show="isAddButtonVisible"
x-transition
@@ -235,7 +235,7 @@
</ul>
@endif
@if ($addAction)
@if ($addAction->isVisible())
<x-filament-forms::builder.block-picker
:action="$addAction"
:blocks="$getBlocks()"
@@ -26,6 +26,8 @@
this.checkIfAllCheckboxesAreChecked()
Livewire.hook('message.processed', () => {
this.updateVisibleCheckboxListOptions()
this.checkIfAllCheckboxesAreChecked()
})
@@ -46,11 +46,11 @@
{{ $getValueLabel() }}
</th>
@if (($deleteAction || $reorderAction) && (! $isDisabled))
@if (($deleteAction->isVisible() || $reorderAction->isVisible()) && (! $isDisabled))
<th
scope="col"
x-show="rows.length"
class="{{ ($deleteAction && $reorderAction) ? 'w-16' : 'w-12' }}"
class="{{ ($deleteAction->isVisible() && $reorderAction->isVisible()) ? 'w-16' : 'w-12' }}"
>
<span class="sr-only"></span>
</th>
@@ -59,7 +59,7 @@
</thead>
<tbody
@if ($reorderAction)
@if ($reorderAction->isVisible())
x-sortable
x-on:end="reorderRows($event)"
@endif
@@ -72,7 +72,7 @@
x-ref="rowTemplate"
>
<tr
@if ($reorderAction)
@if ($reorderAction->isVisible())
x-bind:x-sortable-item="row.key"
@endif
class="divide-x rtl:divide-x-reverse dark:divide-gray-600"
@@ -103,18 +103,18 @@
/>
</td>
@if (($deleteAction || $reorderAction) && (! $isDisabled))
@if (($deleteAction->isVisible() || $reorderAction->isVisible()) && (! $isDisabled))
<td class="whitespace-nowrap">
<div
class="flex items-center justify-center px-2"
>
@if ($reorderAction)
@if ($reorderAction->isVisible())
<div x-sortable-handle>
{{ $reorderAction }}
</div>
@endif
@if ($deleteAction)
@if ($deleteAction->isVisible())
<div
x-on:click="deleteRow(index)"
>
@@ -129,7 +129,7 @@
</tbody>
</table>
@if ($addAction && (! $isDisabled))
@if ($addAction->isVisible() && (! $isDisabled))
<div x-on:click="addRow" class="px-4 py-3">
{{ $addAction }}
</div>
@@ -92,7 +92,7 @@
"
class="filament-forms-repeater-component-item relative rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-800 dark:ring-white/20"
>
@if ($reorderAction || $deleteAction || $cloneAction || $isCollapsible || $hasItemLabels)
@if ($reorderAction->isVisible() || $cloneAction->isVisible() || $deleteAction->isVisible() || $isCollapsible || $hasItemLabels)
<header
@if ($isCollapsible) x-on:click.stop="isCollapsed = ! isCollapsed" @endif
@class([
@@ -100,7 +100,7 @@
'cursor-pointer' => $isCollapsible,
])
>
@if ($reorderAction)
@if ($reorderAction->isVisible())
<div x-sortable-handle>
{{ $reorderAction }}
</div>
@@ -135,7 +135,7 @@
@endif
@endif
@if ($cloneAction)
@if ($cloneAction->isVisible())
<li
class="flex items-center justify-center"
>
@@ -143,7 +143,7 @@
</li>
@endif
@if ($deleteAction)
@if ($deleteAction->isVisible())
<li
class="flex items-center justify-center"
>
@@ -193,7 +193,7 @@
</ul>
@endif
@if ($addAction)
@if ($addAction->isVisible())
<div class="relative flex justify-center">
{{ $addAction }}
</div>
+36 -57
View File
@@ -79,17 +79,17 @@ class Builder extends Field implements Contracts\CanConcealComponents
});
$this->registerActions([
fn (Builder $component): ?Action => $component->getAddAction(),
fn (Builder $component): ?Action => $component->getAddBetweenAction(),
fn (Builder $component): ?Action => $component->getCloneAction(),
fn (Builder $component): ?Action => $component->getCollapseAction(),
fn (Builder $component): ?Action => $component->getCollapseAllAction(),
fn (Builder $component): ?Action => $component->getDeleteAction(),
fn (Builder $component): ?Action => $component->getExpandAction(),
fn (Builder $component): ?Action => $component->getExpandAllAction(),
fn (Builder $component): ?Action => $component->getMoveDownAction(),
fn (Builder $component): ?Action => $component->getMoveUpAction(),
fn (Builder $component): ?Action => $component->getReorderAction(),
fn (Builder $component): Action => $component->getAddAction(),
fn (Builder $component): Action => $component->getAddBetweenAction(),
fn (Builder $component): Action => $component->getCloneAction(),
fn (Builder $component): Action => $component->getCollapseAction(),
fn (Builder $component): Action => $component->getCollapseAllAction(),
fn (Builder $component): Action => $component->getDeleteAction(),
fn (Builder $component): Action => $component->getExpandAction(),
fn (Builder $component): Action => $component->getExpandAllAction(),
fn (Builder $component): Action => $component->getMoveDownAction(),
fn (Builder $component): Action => $component->getMoveUpAction(),
fn (Builder $component): Action => $component->getReorderAction(),
]);
$this->mutateDehydratedStateUsing(static function (?array $state): array {
@@ -107,12 +107,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return $this;
}
public function getAddAction(): ?Action
public function getAddAction(): Action
{
if (! $this->isAddable()) {
return null;
}
$action = Action::make($this->getAddActionName())
->label(fn (Builder $component) => $component->getAddActionLabel())
->action(function (array $arguments, Builder $component): void {
@@ -133,7 +129,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
->livewireClickHandlerEnabled(false)
->button()
->outlined()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isAddable());
if ($this->modifyAddActionUsing) {
$action = $this->evaluate($this->modifyAddActionUsing, [
@@ -156,12 +153,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'add';
}
public function getAddBetweenAction(): ?Action
public function getAddBetweenAction(): Action
{
if (! $this->isAddable()) {
return null;
}
$action = Action::make($this->getAddBetweenActionName())
->label(fn (Builder $component) => $component->getAddBetweenActionLabel())
->icon('heroicon-m-plus')
@@ -189,7 +182,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
})
->livewireClickHandlerEnabled(false)
->iconButton()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isAddable());
if ($this->modifyAddBetweenActionUsing) {
$action = $this->evaluate($this->modifyAddBetweenActionUsing, [
@@ -212,12 +206,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'addBetween';
}
public function getCloneAction(): ?Action
public function getCloneAction(): Action
{
if (! $this->isCloneable()) {
return null;
}
$action = Action::make($this->getCloneActionName())
->label(__('filament-forms::components.builder.actions.clone.label'))
->icon('heroicon-m-square-2-stack')
@@ -234,7 +224,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isCloneable());
if ($this->modifyCloneActionUsing) {
$action = $this->evaluate($this->modifyCloneActionUsing, [
@@ -257,12 +248,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'clone';
}
public function getDeleteAction(): ?Action
public function getDeleteAction(): Action
{
if (! $this->isDeletable()) {
return null;
}
$action = Action::make($this->getDeleteActionName())
->label(__('filament-forms::components.builder.actions.delete.label'))
->icon('heroicon-m-trash')
@@ -275,7 +262,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isDeletable());
if ($this->modifyDeleteActionUsing) {
$action = $this->evaluate($this->modifyDeleteActionUsing, [
@@ -298,12 +286,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'delete';
}
public function getMoveDownAction(): ?Action
public function getMoveDownAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getMoveDownActionName())
->label(__('filament-forms::components.builder.actions.move_down.label'))
->icon('heroicon-m-chevron-down')
@@ -315,7 +299,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyMoveDownActionUsing) {
$action = $this->evaluate($this->modifyMoveDownActionUsing, [
@@ -338,12 +323,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'moveDown';
}
public function getMoveUpAction(): ?Action
public function getMoveUpAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getMoveUpActionName())
->label(__('filament-forms::components.builder.actions.move_up.label'))
->icon('heroicon-m-chevron-up')
@@ -355,7 +336,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyMoveUpActionUsing) {
$action = $this->evaluate($this->modifyMoveUpActionUsing, [
@@ -378,12 +360,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'moveUp';
}
public function getReorderAction(): ?Action
public function getReorderAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getReorderActionName())
->label(__('filament-forms::components.builder.actions.reorder.label'))
->icon('heroicon-m-arrows-up-down')
@@ -399,7 +377,8 @@ class Builder extends Field implements Contracts\CanConcealComponents
->livewireClickHandlerEnabled(false)
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyReorderActionUsing) {
$action = $this->evaluate($this->modifyReorderActionUsing, [
@@ -422,7 +401,7 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'reorder';
}
public function getCollapseAction(): ?Action
public function getCollapseAction(): Action
{
$action = Action::make($this->getCollapseActionName())
->label(__('filament-forms::components.builder.actions.collapse.label'))
@@ -454,7 +433,7 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'collapse';
}
public function getExpandAction(): ?Action
public function getExpandAction(): Action
{
$action = Action::make($this->getExpandActionName())
->label(__('filament-forms::components.builder.actions.expand.label'))
@@ -486,7 +465,7 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'expand';
}
public function getCollapseAllAction(): ?Action
public function getCollapseAllAction(): Action
{
$action = Action::make($this->getCollapseAllActionName())
->label(__('filament-forms::components.builder.actions.collapse_all.label'))
@@ -515,7 +494,7 @@ class Builder extends Field implements Contracts\CanConcealComponents
return 'collapseAll';
}
public function getExpandAllAction(): ?Action
public function getExpandAllAction(): Action
{
$action = Action::make($this->getExpandAllActionName())
->label(__('filament-forms::components.builder.actions.expand_all.label'))
@@ -50,12 +50,12 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
$this->searchDebounce(0);
$this->registerActions([
fn (CheckboxList $component): ?Action => $component->getSelectAllAction(),
fn (CheckboxList $component): ?Action => $component->getDeselectAllAction(),
fn (CheckboxList $component): Action => $component->getSelectAllAction(),
fn (CheckboxList $component): Action => $component->getDeselectAllAction(),
]);
}
public function getSelectAllAction(): ?Action
public function getSelectAllAction(): Action
{
$action = Action::make($this->getSelectAllActionName())
->label(__('filament-forms::components.checkbox_list.actions.select_all.label'))
@@ -84,7 +84,7 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
return 'selectAll';
}
public function getDeselectAllAction(): ?Action
public function getDeselectAllAction(): Action
{
$action = Action::make($this->getDeselectAllActionName())
->label(__('filament-forms::components.checkbox_list.actions.deselect_all.label'))
@@ -113,18 +113,18 @@ class CheckboxList extends Field implements Contracts\HasNestedRecursiveValidati
return 'deselectAll';
}
public function relationship(string | Closure | null $relationshipName, string | Closure | null $titleAttribute, ?Closure $modifyOptionsQueryUsing = null): static
public function relationship(string | Closure | null $name, string | Closure | null $titleAttribute, ?Closure $modifyQueryUsing = null): static
{
$this->relationship = $relationshipName ?? $this->getName();
$this->relationship = $name ?? $this->getName();
$this->relationshipTitleAttribute = $titleAttribute;
$this->options(static function (CheckboxList $component) use ($modifyOptionsQueryUsing): array {
$this->options(static function (CheckboxList $component) use ($modifyQueryUsing): array {
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->query();
if ($modifyOptionsQueryUsing) {
$relationshipQuery = $component->evaluate($modifyOptionsQueryUsing, [
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
'query' => $relationshipQuery,
]) ?? $relationshipQuery;
}
@@ -168,16 +168,16 @@ trait CanBeValidated
return $this;
}
public function exists(string | Closure | null $table = null, string | Closure | null $column = null, ?Closure $callback = null): static
public function exists(string | Closure | null $table = null, string | Closure | null $column = null, ?Closure $modifyRuleUsing = null): static
{
$this->rule(static function (Field $component, ?string $model) use ($callback, $column, $table) {
$this->rule(static function (Field $component, ?string $model) use ($column, $modifyRuleUsing, $table) {
$table = $component->evaluate($table) ?? $model;
$column = $component->evaluate($column) ?? $component->getName();
$rule = Rule::exists($table, $column);
if ($callback) {
$rule = $component->evaluate($callback, [
if ($modifyRuleUsing) {
$rule = $component->evaluate($modifyRuleUsing, [
'rule' => $rule,
]) ?? $rule;
}
@@ -501,9 +501,9 @@ trait CanBeValidated
return $this->fieldComparisonRule('same', $statePath, $isStatePathAbsolute);
}
public function unique(string | Closure | null $table = null, string | Closure | null $column = null, Model | Closure $ignorable = null, ?Closure $callback = null, bool $ignoreRecord = false): static
public function unique(string | Closure | null $table = null, string | Closure | null $column = null, Model | Closure $ignorable = null, bool $ignoreRecord = false, ?Closure $modifyRuleUsing = null): static
{
$this->rule(static function (Field $component, ?string $model) use ($callback, $column, $ignorable, $table, $ignoreRecord) {
$this->rule(static function (Field $component, ?string $model) use ($column, $ignorable, $ignoreRecord, $modifyRuleUsing, $table) {
$table = $component->evaluate($table) ?? $model;
$column = $component->evaluate($column) ?? $component->getName();
$ignorable = ($ignoreRecord && ! $ignorable) ?
@@ -519,8 +519,8 @@ trait CanBeValidated
),
);
if ($callback) {
$rule = $component->evaluate($callback, [
if ($modifyRuleUsing) {
$rule = $component->evaluate($modifyRuleUsing, [
'rule' => $rule,
]) ?? $rule;
}
@@ -24,10 +24,10 @@ trait EntanglesStateWithSingularRelationship
protected ?Closure $mutateRelationshipDataBeforeSaveUsing = null;
public function relationship(string $relationshipName, bool | Closure $condition = true): static
public function relationship(string $name, bool | Closure $condition = true): static
{
$this->relationship = $relationshipName;
$this->statePath($relationshipName);
$this->relationship = $name;
$this->statePath($name);
$this->loadStateFromRelationshipsUsing(static function (Component | CanEntangleWithSingularRelationships $component) {
$component->clearCachedExistingRecord();
+12 -21
View File
@@ -59,23 +59,20 @@ class KeyValue extends Field
});
$this->registerActions([
fn (KeyValue $component): ?Action => $component->getAddAction(),
fn (KeyValue $component): ?Action => $component->getDeleteAction(),
fn (KeyValue $component): ?Action => $component->getReorderAction(),
fn (KeyValue $component): Action => $component->getAddAction(),
fn (KeyValue $component): Action => $component->getDeleteAction(),
fn (KeyValue $component): Action => $component->getReorderAction(),
]);
}
public function getAddAction(): ?Action
public function getAddAction(): Action
{
if (! $this->isAddable()) {
return null;
}
$action = Action::make($this->getAddActionName())
->label(fn (KeyValue $component) => $component->getAddActionLabel())
->livewireClickHandlerEnabled(false)
->link()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isAddable());
if ($this->modifyAddActionUsing) {
$action = $this->evaluate($this->modifyAddActionUsing, [
@@ -98,12 +95,8 @@ class KeyValue extends Field
return 'add';
}
public function getDeleteAction(): ?Action
public function getDeleteAction(): Action
{
if (! $this->isDeletable()) {
return null;
}
$action = Action::make($this->getDeleteActionName())
->label(__('filament-forms::components.key_value.actions.delete.label'))
->icon('heroicon-m-trash')
@@ -111,7 +104,8 @@ class KeyValue extends Field
->livewireClickHandlerEnabled(false)
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isDeletable());
if ($this->modifyDeleteActionUsing) {
$action = $this->evaluate($this->modifyDeleteActionUsing, [
@@ -134,12 +128,8 @@ class KeyValue extends Field
return 'delete';
}
public function getReorderAction(): ?Action
public function getReorderAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getReorderActionName())
->label(__('filament-forms::components.key_value.actions.reorder.label'))
->icon('heroicon-m-arrows-up-down')
@@ -147,7 +137,8 @@ class KeyValue extends Field
->livewireClickHandlerEnabled(false)
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyReorderActionUsing) {
$action = $this->evaluate($this->modifyReorderActionUsing, [
+32 -50
View File
@@ -92,16 +92,16 @@ class Repeater extends Field implements Contracts\CanConcealComponents
});
$this->registerActions([
fn (Repeater $component): ?Action => $component->getAddAction(),
fn (Repeater $component): ?Action => $component->getCloneAction(),
fn (Repeater $component): ?Action => $component->getCollapseAction(),
fn (Repeater $component): ?Action => $component->getCollapseAllAction(),
fn (Repeater $component): ?Action => $component->getDeleteAction(),
fn (Repeater $component): ?Action => $component->getExpandAction(),
fn (Repeater $component): ?Action => $component->getExpandAllAction(),
fn (Repeater $component): ?Action => $component->getMoveDownAction(),
fn (Repeater $component): ?Action => $component->getMoveUpAction(),
fn (Repeater $component): ?Action => $component->getReorderAction(),
fn (Repeater $component): Action => $component->getAddAction(),
fn (Repeater $component): Action => $component->getCloneAction(),
fn (Repeater $component): Action => $component->getCollapseAction(),
fn (Repeater $component): Action => $component->getCollapseAllAction(),
fn (Repeater $component): Action => $component->getDeleteAction(),
fn (Repeater $component): Action => $component->getExpandAction(),
fn (Repeater $component): Action => $component->getExpandAllAction(),
fn (Repeater $component): Action => $component->getMoveDownAction(),
fn (Repeater $component): Action => $component->getMoveUpAction(),
fn (Repeater $component): Action => $component->getReorderAction(),
]);
$this->mutateDehydratedStateUsing(static function (?array $state): array {
@@ -109,12 +109,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
});
}
public function getAddAction(): ?Action
public function getAddAction(): Action
{
if (! $this->isAddable()) {
return null;
}
$action = Action::make($this->getAddActionName())
->label(fn (Repeater $component) => $component->getAddActionLabel())
->action(function (Repeater $component): void {
@@ -131,7 +127,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
})
->button()
->outlined()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isAddable());
if ($this->modifyAddActionUsing) {
$action = $this->evaluate($this->modifyAddActionUsing, [
@@ -154,12 +151,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'add';
}
public function getCloneAction(): ?Action
public function getCloneAction(): Action
{
if (! $this->isCloneable()) {
return null;
}
$action = Action::make($this->getCloneActionName())
->label(__('filament-forms::components.repeater.actions.clone.label'))
->icon('heroicon-m-square-2-stack')
@@ -176,7 +169,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isCloneable());
if ($this->modifyCloneActionUsing) {
$action = $this->evaluate($this->modifyCloneActionUsing, [
@@ -199,12 +193,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'clone';
}
public function getDeleteAction(): ?Action
public function getDeleteAction(): Action
{
if (! $this->isDeletable()) {
return null;
}
$action = Action::make($this->getDeleteActionName())
->label(__('filament-forms::components.repeater.actions.delete.label'))
->icon('heroicon-m-trash')
@@ -217,7 +207,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isDeletable());
if ($this->modifyDeleteActionUsing) {
$action = $this->evaluate($this->modifyDeleteActionUsing, [
@@ -240,12 +231,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'delete';
}
public function getMoveDownAction(): ?Action
public function getMoveDownAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getMoveDownActionName())
->label(__('filament-forms::components.repeater.actions.move_down.label'))
->icon('heroicon-m-chevron-down')
@@ -257,7 +244,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyMoveDownActionUsing) {
$action = $this->evaluate($this->modifyMoveDownActionUsing, [
@@ -280,12 +268,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'moveDown';
}
public function getMoveUpAction(): ?Action
public function getMoveUpAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getMoveUpActionName())
->label(__('filament-forms::components.repeater.actions.move_up.label'))
->icon('heroicon-m-chevron-up')
@@ -297,7 +281,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
})
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyMoveUpActionUsing) {
$action = $this->evaluate($this->modifyMoveUpActionUsing, [
@@ -320,12 +305,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'moveUp';
}
public function getReorderAction(): ?Action
public function getReorderAction(): Action
{
if (! $this->isReorderable()) {
return null;
}
$action = Action::make($this->getReorderActionName())
->label(__('filament-forms::components.repeater.actions.reorder.label'))
->icon('heroicon-m-arrows-up-down')
@@ -341,7 +322,8 @@ class Repeater extends Field implements Contracts\CanConcealComponents
->livewireClickHandlerEnabled(false)
->iconButton()
->inline()
->size('sm');
->size('sm')
->visible(fn (): bool => $this->isReorderable());
if ($this->modifyReorderActionUsing) {
$action = $this->evaluate($this->modifyReorderActionUsing, [
@@ -364,7 +346,7 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'reorder';
}
public function getCollapseAction(): ?Action
public function getCollapseAction(): Action
{
$action = Action::make($this->getCollapseActionName())
->label(__('filament-forms::components.repeater.actions.collapse.label'))
@@ -396,7 +378,7 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'collapse';
}
public function getExpandAction(): ?Action
public function getExpandAction(): Action
{
$action = Action::make($this->getExpandActionName())
->label(__('filament-forms::components.repeater.actions.expand.label'))
@@ -428,7 +410,7 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'expand';
}
public function getCollapseAllAction(): ?Action
public function getCollapseAllAction(): Action
{
$action = Action::make($this->getCollapseAllActionName())
->label(__('filament-forms::components.repeater.actions.collapse_all.label'))
@@ -457,7 +439,7 @@ class Repeater extends Field implements Contracts\CanConcealComponents
return 'collapseAll';
}
public function getExpandAllAction(): ?Action
public function getExpandAllAction(): Action
{
$action = Action::make($this->getExpandAllActionName())
->label(__('filament-forms::components.repeater.actions.expand_all.label'))
+14 -14
View File
@@ -618,18 +618,18 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
return $this->evaluate($this->isSearchable) || $this->isMultiple();
}
public function relationship(string | Closure | null $relationshipName, string | Closure | null $titleAttribute, ?Closure $modifyOptionsQueryUsing = null): static
public function relationship(string | Closure | null $name, string | Closure | null $titleAttribute, ?Closure $modifyQueryUsing = null): static
{
$this->relationship = $relationshipName ?? $this->getName();
$this->relationship = $name ?? $this->getName();
$this->relationshipTitleAttribute = $titleAttribute;
$this->getSearchResultsUsing(static function (Select $component, ?string $search) use ($modifyOptionsQueryUsing): array {
$this->getSearchResultsUsing(static function (Select $component, ?string $search) use ($modifyQueryUsing): array {
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->query();
if ($modifyOptionsQueryUsing) {
$relationshipQuery = $component->evaluate($modifyOptionsQueryUsing, [
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
'query' => $relationshipQuery,
]) ?? $relationshipQuery;
}
@@ -680,7 +680,7 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
->toArray();
});
$this->options(static function (Select $component) use ($modifyOptionsQueryUsing): ?array {
$this->options(static function (Select $component) use ($modifyQueryUsing): ?array {
if (($component->isSearchable()) && ! $component->isPreloaded()) {
return null;
}
@@ -689,8 +689,8 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
$relationshipQuery = $relationship->getRelated()->query();
if ($modifyOptionsQueryUsing) {
$relationshipQuery = $component->evaluate($modifyOptionsQueryUsing, [
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
'query' => $relationshipQuery,
]) ?? $relationshipQuery;
}
@@ -780,13 +780,13 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
return $record->getAttributeValue($component->getRelationshipTitleAttribute());
});
$this->getSelectedRecordUsing(static function (Select $component, $state) use ($modifyOptionsQueryUsing): ?Model {
$this->getSelectedRecordUsing(static function (Select $component, $state) use ($modifyQueryUsing): ?Model {
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->query()->where($relationship->getOwnerKeyName(), $state);
if ($modifyOptionsQueryUsing) {
$relationshipQuery = $component->evaluate($modifyOptionsQueryUsing, [
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
'query' => $relationshipQuery,
]) ?? $relationshipQuery;
}
@@ -794,15 +794,15 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe
return $relationshipQuery->first();
});
$this->getOptionLabelsUsing(static function (Select $component, array $values) use ($modifyOptionsQueryUsing): array {
$this->getOptionLabelsUsing(static function (Select $component, array $values) use ($modifyQueryUsing): array {
$relationship = $component->getRelationship();
$relatedKeyName = $relationship->getRelatedKeyName();
$relationshipQuery = $relationship->getRelated()->query()
->whereIn($relatedKeyName, $values);
if ($modifyOptionsQueryUsing) {
$relationshipQuery = $component->evaluate($modifyOptionsQueryUsing, [
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
'query' => $relationshipQuery,
]) ?? $relationshipQuery;
}
+4 -4
View File
@@ -56,8 +56,8 @@ class Wizard extends Component
parent::setUp();
$this->registerActions([
fn (Wizard $component): ?Action => $component->getNextAction(),
fn (Wizard $component): ?Action => $component->getPreviousAction(),
fn (Wizard $component): Action => $component->getNextAction(),
fn (Wizard $component): Action => $component->getPreviousAction(),
]);
$this->registerListeners([
@@ -84,7 +84,7 @@ class Wizard extends Component
]);
}
public function getNextAction(): ?Action
public function getNextAction(): Action
{
$action = Action::make($this->getNextActionName())
->label(__('filament-forms::components.wizard.actions.next_step.label'))
@@ -116,7 +116,7 @@ class Wizard extends Component
return 'next';
}
public function getPreviousAction(): ?Action
public function getPreviousAction(): Action
{
$action = Action::make($this->getPreviousActionName())
->label(__('filament-forms::components.wizard.actions.previous_step.label'))
@@ -33,21 +33,21 @@ trait HasComponents
return $this;
}
public function getComponent(string | Closure $callback, bool $withHidden = false): ?Component
public function getComponent(string | Closure $findComponentUsing, bool $withHidden = false): ?Component
{
if (is_string($callback)) {
$callback = static function (Component $component) use ($callback): bool {
if (is_string($findComponentUsing)) {
$findComponentUsing = static function (Component $component) use ($findComponentUsing): bool {
$key = $component->getKey();
if ($key === null) {
return false;
}
return $key === $callback;
return $key === $findComponentUsing;
};
}
return collect($this->getFlatComponents($withHidden))->first($callback);
return collect($this->getFlatComponents($withHidden))->first($findComponentUsing);
}
/**
+4 -4
View File
@@ -138,9 +138,9 @@ class TestsForms
public function assertFormFieldExists(): Closure
{
return function (string $fieldName, string | Closure $formName = 'form', ?Closure $callback = null): static {
return function (string $fieldName, string | Closure $formName = 'form', ?Closure $checkFieldUsing = null): static {
if ($formName instanceof Closure) {
$callback = $formName;
$checkFieldUsing = $formName;
$formName = 'form';
}
@@ -161,9 +161,9 @@ class TestsForms
"Failed asserting that a field with the name [{$fieldName}] exists on the form with the name [{$formName}] on the [{$livewireClass}] component."
);
if ($callback) {
if ($checkFieldUsing) {
Assert::assertTrue(
$callback($field),
$checkFieldUsing($field),
"Failed asserting that a field with the name [{$fieldName}] and provided configuration exists on the form with the name [{$formName}] on the [{$livewireClass}] component."
);
}
@@ -4,9 +4,9 @@ namespace Filament\Infolists\Components\Concerns;
trait EntanglesStateWithSingularRelationship
{
public function relationship(string $relationshipName): static
public function relationship(string $name): static
{
$this->statePath($relationshipName);
$this->statePath($name);
return $this;
}
+1
View File
@@ -31,6 +31,7 @@
},
"autoload": {
"files": [
"src/global_helpers.php",
"src/helpers.php"
],
"psr-4": {
@@ -191,12 +191,12 @@ For authorization, Filament will observe any [model policies](https://laravel.co
- `restore()` is used to prevent a single soft-deleted record from being restored. `restoreAny()` is used to prevent records from being bulk restored. Filament uses the `restoreAny()` method because iterating through multiple records and checking the `restore()` policy is not very performant.
- `reorder()` is used to control [reordering a record](listing-records#reordering-records).
### Ignoring authorization policies
### Skipping authorization
If you'd like to ignore policies for a resource, you may set the `$shouldIgnorePolicies` property to `false`:
If you'd like to skip authorization for a resource, you may set the `$shouldSkipAuthorization` property to `true`:
```php
protected static bool $shouldIgnorePolicies = true;
protected static bool $shouldSkipAuthorization = true;
```
## Customizing the model label
+1 -1
View File
@@ -166,7 +166,7 @@ class BlogPlugin implements Plugin
{
public static function get(): static
{
return filament($this->getId());
return filament(app(static::class)->getId());
}
// ...
@@ -10,9 +10,7 @@
x-on:open-global-search-results.window="isOpen = true"
{{ $attributes->class(['filament-global-search-results-container absolute end-0 top-auto z-10 mt-2 w-screen max-w-xs overflow-hidden rounded-xl shadow-xl ring-1 ring-gray-950/5 dark:ring-white/20 sm:max-w-lg']) }}
>
<div
class="max-h-96 overflow-x-hidden overflow-y-scroll bg-white dark:bg-gray-800"
>
<div class="max-h-96 overflow-x-hidden bg-white dark:bg-gray-800">
@forelse ($results->getCategories() as $group => $groupedResults)
<x-filament::global-search.result-group
:label="$group"
+2 -2
View File
@@ -612,10 +612,10 @@ class FilamentManager
}
}
public function registerRenderHook(string $name, Closure $callback): void
public function registerRenderHook(string $name, Closure $hook): void
{
try {
$this->getDefaultPanel()->renderHook($name, $callback);
$this->getDefaultPanel()->renderHook($name, $hook);
} catch (NoDefaultPanelSetException $exception) {
throw new Exception('Please use the `renderHook()` method on the panel configuration to register render hooks. See the documentation - https://filamentphp.com/docs/panels/configuration#render-hooks');
}
@@ -63,7 +63,8 @@ class NavigationGroup
continue;
}
throw new Exception("Navigation group [{$this->getLabel()}] has a nested group, which is not supported.");
/** @phpstan-ignore-next-line */
throw new Exception("Navigation group [{$this->getLabel()}] has a nested group, which is not supported in the sidebar design at the moment.");
}
$this->items = $items;
@@ -14,12 +14,18 @@ trait HasRoutes
*/
protected static string | array $routeMiddleware = [];
/**
* @var string | array<string>
*/
protected static string | array $withoutRouteMiddleware = [];
public static function routes(Panel $panel): void
{
$slug = static::getSlug();
Route::get("/{$slug}", static::class)
->middleware(static::getRouteMiddleware($panel))
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
->name((string) str($slug)->replace('/', '.'));
}
@@ -42,6 +48,14 @@ trait HasRoutes
];
}
/**
* @return string | array<string>
*/
public static function getWithoutRouteMiddleware(Panel $panel): string | array
{
return static::$withoutRouteMiddleware;
}
public static function getEmailVerifiedMiddleware(Panel $panel): string
{
return $panel->getEmailVerifiedMiddleware();
@@ -13,9 +13,9 @@ trait HasRenderHooks
*/
protected array $renderHooks = [];
public function renderHook(string $name, Closure $callback): static
public function renderHook(string $name, Closure $hook): static
{
$this->renderHooks[$name][] = $callback;
$this->renderHooks[$name][] = $hook;
return $this;
}
@@ -2,7 +2,7 @@
namespace Filament\Resources\RelationManagers;
use Filament\Facades\Filament;
use function Filament\authorize;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Infolists\Infolist;
@@ -10,11 +10,11 @@ use Filament\Resources\Pages\ViewRecord;
use Filament\Tables;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Table;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Livewire\Component;
use Livewire\Attributes\Locked;
@@ -82,9 +82,9 @@ class RelationManager extends Component implements Forms\Contracts\HasForms, Tab
*/
protected static ?string $pluralModelLabel = null;
protected static bool $shouldAuthorizeWithGate = false;
protected static bool $shouldCheckPolicyExistence = true;
protected static bool $shouldIgnorePolicies = false;
protected static bool $shouldSkipAuthorization = false;
public function isReadOnly(): bool
{
@@ -227,71 +227,52 @@ class RelationManager extends Component implements Forms\Contracts\HasForms, Tab
protected function can(string $action, ?Model $record = null): bool
{
$user = Filament::auth()->user();
if (static::shouldSkipAuthorization()) {
return true;
}
$model = $this->getTable()->getModel();
if (static::shouldAuthorizeWithGate()) {
return Gate::forUser($user)->check($action, $record ?? $model);
try {
return authorize($action, $record ?? $model, static::shouldCheckPolicyExistence())->allowed();
} catch (AuthorizationException $exception) {
return $exception->toResponse()->allowed();
}
if (static::shouldIgnorePolicies()) {
return true;
}
$policy = Gate::getPolicyFor($model);
if ($policy === null) {
return true;
}
if (! method_exists($policy, $action)) {
return true;
}
return Gate::forUser($user)->check($action, $record ?? $model);
}
public static function authorizeWithGate(bool $condition = true): void
{
static::$shouldAuthorizeWithGate = $condition;
}
public static function ignorePolicies(bool $condition = true): void
{
static::$shouldIgnorePolicies = $condition;
}
public static function shouldAuthorizeWithGate(): bool
{
return static::$shouldAuthorizeWithGate;
}
public static function shouldIgnorePolicies(): bool
{
return static::$shouldIgnorePolicies;
}
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
if (static::shouldIgnorePolicies()) {
if (static::shouldSkipAuthorization()) {
return true;
}
$model = $ownerRecord->{static::getRelationshipName()}()->getQuery()->getModel()::class;
$policy = Gate::getPolicyFor($model);
$user = Filament::auth()->user();
$action = 'viewAny';
if ($policy === null) {
return true;
try {
return authorize('viewAll', $model, static::shouldCheckPolicyExistence())->allowed();
} catch (AuthorizationException $exception) {
return $exception->toResponse()->allowed();
}
}
if (! method_exists($policy, $action)) {
return true;
}
public static function checkPolicyExistence(bool $condition = true): void
{
static::$shouldCheckPolicyExistence = $condition;
}
return Gate::forUser($user)->check($action, $model);
public static function skipAuthorization(bool $condition = true): void
{
static::$shouldSkipAuthorization = $condition;
}
public static function shouldCheckPolicyExistence(): bool
{
return static::$shouldCheckPolicyExistence;
}
public static function shouldSkipAuthorization(): bool
{
return static::$shouldSkipAuthorization;
}
public function form(Form $form): Form
+78 -34
View File
@@ -2,6 +2,7 @@
namespace Filament\Resources;
use function Filament\authorize;
use Filament\Facades\Filament;
use Filament\Forms\Form;
use Filament\GlobalSearch\Actions\Action;
@@ -14,13 +15,14 @@ use Filament\Resources\RelationManagers\RelationGroup;
use function Filament\Support\get_model_label;
use function Filament\Support\locale_has_pluralization;
use Filament\Tables\Table;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Access\Response;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
@@ -77,11 +79,16 @@ abstract class Resource
*/
protected static string | array $routeMiddleware = [];
/**
* @var string | array<string>
*/
protected static string | array $withoutRouteMiddleware = [];
protected static int $globalSearchResultsLimit = 50;
protected static bool $shouldAuthorizeWithGate = false;
protected static bool $shouldCheckPolicyExistence = true;
protected static bool $shouldIgnorePolicies = false;
protected static bool $shouldSkipAuthorization = false;
public static function form(Form $form): Form
{
@@ -140,47 +147,55 @@ abstract class Resource
public static function can(string $action, ?Model $record = null): bool
{
$user = Filament::auth()->user();
if (static::shouldSkipAuthorization()) {
return true;
}
$model = static::getModel();
if (static::shouldAuthorizeWithGate()) {
return Gate::forUser($user)->check($action, $record ?? $model);
try {
return authorize($action, $record ?? $model, static::shouldCheckPolicyExistence())->allowed();
} catch (AuthorizationException $exception) {
return $exception->toResponse()->allowed();
}
if (static::shouldIgnorePolicies()) {
return true;
}
$policy = Gate::getPolicyFor($model);
if ($policy === null) {
return true;
}
if (! method_exists($policy, $action)) {
return true;
}
return Gate::forUser($user)->check($action, $record ?? $model);
}
public static function authorizeWithGate(bool $condition = true): void
/**
* @throws AuthorizationException
*/
public static function authorize(string $action, ?Model $record = null): ?Response
{
static::$shouldAuthorizeWithGate = $condition;
if (static::shouldSkipAuthorization()) {
return null;
}
$model = static::getModel();
try {
return authorize($action, $record ?? $model, static::shouldCheckPolicyExistence());
} catch (AuthorizationException $exception) {
return $exception->toResponse();
}
}
public static function ignorePolicies(bool $condition = true): void
public static function checkPolicyExistence(bool $condition = true): void
{
static::$shouldIgnorePolicies = $condition;
static::$shouldCheckPolicyExistence = $condition;
}
public static function shouldAuthorizeWithGate(): bool
public static function skipAuthorization(bool $condition = true): void
{
return static::$shouldAuthorizeWithGate;
static::$shouldSkipAuthorization = $condition;
}
public static function shouldIgnorePolicies(): bool
public static function shouldCheckPolicyExistence(): bool
{
return static::$shouldIgnorePolicies;
return static::$shouldCheckPolicyExistence;
}
public static function shouldSkipAuthorization(): bool
{
return static::$shouldSkipAuthorization;
}
public static function canViewAny(): bool
@@ -238,16 +253,36 @@ abstract class Resource
return static::can('restoreAny');
}
public static function canGloballySearch(): bool
{
return static::$isGloballySearchable && count(static::getGloballySearchableAttributes()) && static::canViewAny();
}
public static function canView(Model $record): bool
{
return static::can('view', $record);
}
public static function authorizeViewAny(): void
{
static::authorize('viewAny');
}
public static function authorizeCreate(): void
{
static::authorize('create');
}
public static function authorizeEdit(Model $record): void
{
static::authorize('update', $record);
}
public static function authorizeView(Model $record): void
{
static::authorize('view', $record);
}
public static function canGloballySearch(): bool
{
return static::$isGloballySearchable && count(static::getGloballySearchableAttributes()) && static::canViewAny();
}
public static function getBreadcrumb(): string
{
return static::$breadcrumb ?? Str::headline(static::getPluralModelLabel());
@@ -463,6 +498,7 @@ abstract class Resource
)
->prefix($slug)
->middleware(static::getRouteMiddleware($panel))
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
->group(function () use ($panel) {
foreach (static::getPages() as $name => $page) {
$page->registerRoute($panel)?->name($name);
@@ -478,6 +514,14 @@ abstract class Resource
return static::$routeMiddleware;
}
/**
* @return string | array<string>
*/
public static function getWithoutRouteMiddleware(Panel $panel): string | array
{
return static::$withoutRouteMiddleware;
}
public static function getEmailVerifiedMiddleware(Panel $panel): string
{
return $panel->getEmailVerifiedMiddleware();
+18
View File
@@ -0,0 +1,18 @@
<?php
use Filament\Contracts\Plugin;
use Filament\FilamentManager;
if (! function_exists('filament')) {
function filament(?string $plugin = null): FilamentManager | Plugin
{
/** @var FilamentManager $filament */
$filament = app('filament');
if ($plugin !== null) {
return $filament->getPlugin($plugin);
}
return $filament;
}
}
+40 -9
View File
@@ -1,18 +1,49 @@
<?php
use Filament\Contracts\Plugin;
use Filament\FilamentManager;
namespace Filament;
if (! function_exists('filament')) {
function filament(?string $plugin = null): FilamentManager | Plugin
use Filament\Facades\Filament;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Access\Response;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
if (! function_exists('authorize')) {
/**
* @throws AuthorizationException
*/
function authorize(string $action, Model | string $model, bool $shouldCheckPolicyExistence = true): Response
{
/** @var FilamentManager $filament */
$filament = app('filament');
$user = Filament::auth()->user();
if ($plugin !== null) {
return $filament->getPlugin($plugin);
if (! $shouldCheckPolicyExistence) {
return Gate::forUser($user)->authorize($action, $model);
}
return $filament;
$policy = Gate::getPolicyFor($model);
if (
($policy === null) ||
(! method_exists($policy, $action))
) {
/** @var bool | Response | null $response */
$response = invade(Gate::forUser($user))->callBeforeCallbacks(
$user,
$action,
[$model],
);
if ($response === false) {
throw new AuthorizationException();
}
if (! $response instanceof Response) {
return Response::allow();
}
return $response->authorize();
}
return Gate::forUser($user)->authorize($action, $model);
}
}
@@ -15,9 +15,9 @@ trait CanValidateInput
/**
* @param array<array-key> $rules
*/
protected function validateInput(Closure $callback, string $field, array $rules, ?Closure $onError = null): string
protected function validateInput(Closure $askUsing, string $field, array $rules, ?Closure $onError = null): string
{
$input = $callback();
$input = $askUsing();
$validator = Validator::make(
[$field => $input],
@@ -31,7 +31,7 @@ trait CanValidateInput
$onError($validator);
}
$input = $this->validateInput($callback, $field, $rules);
$input = $this->validateInput($askUsing, $field, $rules);
}
return $input;
@@ -16,14 +16,14 @@ trait Configurable
*/
protected static array $importantConfigurations = [];
public static function configureUsing(Closure $callback, ?Closure $during = null, bool $isImportant = false): mixed
public static function configureUsing(Closure $modifyUsing, ?Closure $during = null, bool $isImportant = false): mixed
{
if ($isImportant) {
static::$importantConfigurations[static::class] ??= [];
static::$importantConfigurations[static::class][] = $callback;
static::$importantConfigurations[static::class][] = $modifyUsing;
} else {
static::$configurations[static::class] ??= [];
static::$configurations[static::class][] = $callback;
static::$configurations[static::class][] = $modifyUsing;
}
if (! $during) {
+7 -3
View File
@@ -1,6 +1,7 @@
---
title: Summaries
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
## Overview
@@ -25,15 +26,18 @@ use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\Summarizers\Range;
use Filament\Tables\Columns\TextColumn;
Tables\Columns\TextColumn::make('rating')
TextColumn::make('rating')
->numeric()
->summarize([
Tables\Columns\Summarizers\Average::make(),
Tables\Columns\Summarizers\Range::make(),
Average::make(),
Range::make(),
])
```
> Note: The first column in a table may not use summarizers. That column is used to render the heading and subheading/s of the summary section.
<AutoScreenshot name="tables/summaries" alt="Table with summaries" version="3.x" />
## Available summarizers
Filament ships with four types of summarizer:
+5
View File
@@ -1,6 +1,7 @@
---
title: Grouping rows
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
## Overview
@@ -22,6 +23,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/grouping" alt="Table with grouping" version="3.x" />
## Allowing users to choose between groupings
You may also allow users to pick between different groupings, by passing them in an array to the `groups()` method:
@@ -125,6 +128,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/grouping-descriptions" alt="Table with group descriptions" version="3.x" />
## Collapsible groups
You can allow rows inside a group to be collapsed underneath their group title. To enable this, use a `Group` object with the `collapsible()` method:
+12 -1
View File
@@ -1,11 +1,14 @@
---
title: Empty state
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
## Overview
The table's "empty state" is rendered when there are no rows in the table.
<AutoScreenshot name="tables/empty-state" alt="Table with empty state" version="3.x" />
## Setting the empty state heading
To customize the heading of the empty state, use the `emptyStateHeading()` method:
@@ -20,6 +23,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/empty-state-heading" alt="Table with customized empty state heading" version="3.x" />
## Setting the empty state description
To customize the description of the empty state, use the `emptyStateDescription()` method:
@@ -30,10 +35,12 @@ use Filament\Tables\Table;
public function table(Table $table): Table
{
return $table
->emptyStateDescription('No posts yet');
->emptyStateDescription('Once you write your first post, it will appear here.');
}
```
<AutoScreenshot name="tables/empty-state-description" alt="Table with empty state description" version="3.x" />
## Setting the empty state icon
To customize the [icon](https://blade-ui-kit.com/blade-icons?set=1#search) of the empty state, use the `emptyStateIcon()` method:
@@ -48,6 +55,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/empty-state-icon" alt="Table with customized empty state icon" version="3.x" />
## Adding empty state actions
You can add [actions](actions) to the empty state to prompt users to take action. Pass these to the `emptyStateActions()` method:
@@ -69,6 +78,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/empty-state-actions" alt="Table with empty state actions" version="3.x" />
## Using a custom empty state view
You may use a completely custom empty state view by passing it to the `emptyState()` method:
+7 -3
View File
@@ -1,6 +1,7 @@
---
title: Advanced
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
## Pagination
@@ -103,6 +104,8 @@ If you're using mass assignment protection on your model, you will also need to
When making the table reorderable, a new button will be available on the table to toggle reordering.
<AutoScreenshot name="tables/reordering" alt="Table with reorderable rows" version="3.x" />
The `reorderable()` method accepts the name of a column to store the record order in. If you use something like [`spatie/eloquent-sortable`](https://github.com/spatie/eloquent-sortable) with an order column such as `order_column`, you may use this instead:
```php
@@ -140,9 +143,6 @@ use Filament\Tables\Table;
public function table(Table $table): Table
{
return $table
->filters([
// ...
])
->reorderRecordsTriggerAction(
fn (Action $action, bool $isReordering) => $action
->button()
@@ -151,6 +151,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/reordering/custom-trigger-action" alt="Table with reorderable rows and a custom trigger action" version="3.x" />
## Polling table content
You may poll table content so that it refreshes at a set interval, using the `$form->poll()` method:
@@ -236,6 +238,8 @@ public function table(Table $table): Table
}
```
<AutoScreenshot name="tables/striped" alt="Table with striped rows" version="3.x" />
### Custom row classes
You may want to conditionally style rows based on the record data. This can be achieved by specifying a string or array of CSS classes to be applied to the row using the `$table->recordClasses()` method:
@@ -23,6 +23,7 @@ return [
'search' => [
'label' => 'Sök',
'placeholder' => 'Sök',
'indicator' => 'Sök',
],
],
@@ -65,6 +66,34 @@ return [
],
'summary' => [
'heading' => 'Sammanfattning',
'subheadings' => [
'all' => 'Alla :label',
'group' => ':group sammanfattning',
'page' => 'Denna sida',
],
'summarizers' => [
'average' => [
'label' => 'Medelvärde',
],
'count' => [
'label' => 'Antal',
],
'sum' => [
'label' => 'Summa',
],
],
],
'buttons' => [
'disable_reordering' => [
@@ -79,6 +108,10 @@ return [
'label' => 'Filter',
],
'group' => [
'label' => 'Gruppera',
],
'open_bulk_actions' => [
'label' => 'Öppna åtgärder',
],
@@ -136,6 +169,30 @@ return [
],
'grouping' => [
'fields' => [
'group' => [
'label' => 'Gruppera',
'placeholder' => 'Välj ett fält för gruppering',
],
'direction' => [
'label' => 'Riktning',
'options' => [
'asc' => 'Stigande',
'desc' => 'Fallande',
],
],
],
],
'reorder_indicator' => 'Dra och släpp raderna i önskad ordning.',
'selection_indicator' => [
@@ -10,11 +10,11 @@ trait HasRelationship
{
protected ?Closure $modifyRelationshipQueryUsing = null;
public function relationship(string $relationshipName, string $titleAttribute = null, Closure $callback = null): static
public function relationship(string $name, string $titleAttribute = null, ?Closure $modifyQueryUsing = null): static
{
$this->attribute("{$relationshipName}.{$titleAttribute}");
$this->attribute("{$name}.{$titleAttribute}");
$this->modifyRelationshipQueryUsing = $callback;
$this->modifyRelationshipQueryUsing = $modifyQueryUsing;
return $this;
}
@@ -144,4 +144,17 @@ class TernaryFilter extends SelectFilter
return $this;
}
public function getDefaultState(): mixed
{
$defaultState = $this->evaluate($this->defaultState);
// Ensure that the default state is cast to an integer
// so that it matches the value of a select option.
if (is_bool($defaultState)) {
$defaultState = $defaultState ? 1 : 0;
}
return $defaultState;
}
}
@@ -4,6 +4,7 @@ namespace Filament\Upgrade\Rector;
use Closure;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
@@ -45,6 +46,18 @@ class SimplePropertyChangesRector extends AbstractRector
},
],
],
[
'class' => [
'Filament\\Resources\\Resource',
'Filament\\Resources\\RelationManagers\\RelationManager',
],
'classIdentifier' => 'extends',
'changes' => [
'shouldIgnorePolicies' => function (Property $node) {
$node->props[0] = new Identifier('shouldSkipAuthorization');
},
],
],
];
}