Merge branch '3.x' into upg3
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"packages/panels/src/global_helpers.php",
|
||||
"packages/panels/src/helpers.php",
|
||||
"packages/support/src/helpers.php"
|
||||
],
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 231 KiB |
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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}")
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||