Skip to content

Commit b4aece7

Browse files
authored
Optimize table rendering (#19551)
* Optimize table rendering performance * Refactor loading targets wire target * Update attribute bag * Refactor attribute bag * fix more imports * cleanup * cleanup * fixes * Update TextColumn.php * cleanup * fix * Update HasColor.php * Update ComponentAttributeBag.php * fixes * cleanup * fix tests * fixes * fixes * fixes * tests
1 parent a2f6157 commit b4aece7

214 files changed

Lines changed: 3205 additions & 416 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,41 +141,41 @@ jobs:
141141
- name: Execute tests (parallel)
142142
id: parallel-tests
143143
continue-on-error: true
144-
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --exclude-group=commands
144+
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --exclude-group=serial
145145
env:
146146
DB_PASSWORD: password
147147
- name: Retry tests (parallel, attempt 2)
148148
if: steps.parallel-tests.outcome == 'failure'
149149
id: parallel-retry-2
150150
continue-on-error: true
151-
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=commands
151+
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=serial
152152
env:
153153
DB_PASSWORD: password
154154
- name: Retry tests (parallel, attempt 3)
155155
if: steps.parallel-retry-2.outcome == 'failure'
156156
id: parallel-retry-3
157157
continue-on-error: true
158-
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=commands
158+
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=serial
159159
env:
160160
DB_PASSWORD: password
161161
- name: Retry tests (parallel, attempt 4)
162162
if: steps.parallel-retry-3.outcome == 'failure'
163163
id: parallel-retry-4
164164
continue-on-error: true
165-
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=commands
165+
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=serial
166166
env:
167167
DB_PASSWORD: password
168168
- name: Retry tests (parallel, attempt 5)
169169
if: steps.parallel-retry-4.outcome == 'failure'
170170
id: parallel-retry-5
171-
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=commands
171+
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --parallel --bail --exclude-group=serial
172172
env:
173173
DB_PASSWORD: password
174174
- name: Warn about flaky tests
175175
if: steps.parallel-tests.outcome == 'failure' && (steps.parallel-retry-2.outcome == 'success' || steps.parallel-retry-3.outcome == 'success' || steps.parallel-retry-4.outcome == 'success' || steps.parallel-retry-5.outcome == 'success')
176176
run: |
177177
echo "::warning::Parallel tests failed but a retry passed. This may indicate a race condition or test isolation issue."
178-
- name: Execute command tests (serial)
179-
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --group=commands
178+
- name: Execute serial tests
179+
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml --group=serial
180180
env:
181181
DB_PASSWORD: password

composer.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@
8484
],
8585
"pint": "pint --config pint-strict-imports.json",
8686
"rector": "rector",
87-
"test:pest": "pest --parallel --exclude-group=commands",
88-
"test:sqlite": "pest --configuration=phpunit.sqlite.xml --parallel --exclude-group=commands",
89-
"test:mysql": "pest --configuration=phpunit.mysql.xml --parallel --exclude-group=commands",
90-
"test:pgsql": "pest --configuration=phpunit.pgsql.xml --parallel --exclude-group=commands",
91-
"test:commands:sqlite": "pest --configuration=phpunit.sqlite.xml --group=commands",
92-
"test:commands:mysql": "pest --configuration=phpunit.mysql.xml --group=commands",
93-
"test:commands:pgsql": "pest --configuration=phpunit.pgsql.xml --group=commands",
87+
"test:pest": "pest --parallel --exclude-group=serial",
88+
"test:sqlite": "pest --configuration=phpunit.sqlite.xml --parallel --exclude-group=serial",
89+
"test:mysql": "pest --configuration=phpunit.mysql.xml --parallel --exclude-group=serial",
90+
"test:pgsql": "pest --configuration=phpunit.pgsql.xml --parallel --exclude-group=serial",
91+
"test:serial:sqlite": "pest --configuration=phpunit.sqlite.xml --group=serial",
92+
"test:serial:mysql": "pest --configuration=phpunit.mysql.xml --group=serial",
93+
"test:serial:pgsql": "pest --configuration=phpunit.pgsql.xml --group=serial",
9494
"test:all-databases": [
9595
"@test:sqlite",
9696
"@test:mysql",
@@ -99,7 +99,7 @@
9999
"test:phpstan": "phpstan analyse",
100100
"test": [
101101
"@test:sqlite",
102-
"@test:commands:sqlite",
102+
"@test:serial:sqlite",
103103
"@test:phpstan"
104104
]
105105
},

packages/actions/src/Action.php

Lines changed: 209 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
use Filament\Support\Enums\IconSize;
2121
use Filament\Support\Exceptions\Cancel;
2222
use Filament\Support\Exceptions\Halt;
23+
use Filament\Support\Facades\FilamentColor;
24+
use Filament\Support\View\ComponentAttributeBag as FilamentComponentAttributeBag;
25+
use Filament\Support\View\Components\DropdownComponent\ItemComponent;
26+
use Filament\Support\View\Components\DropdownComponent\ItemComponent\IconComponent;
27+
use Filament\Support\View\Components\LinkComponent;
2328
use Filament\Support\View\Concerns\CanGenerateBadgeHtml;
2429
use Filament\Support\View\Concerns\CanGenerateButtonHtml;
2530
use Filament\Support\View\Concerns\CanGenerateDropdownItemHtml;
@@ -35,9 +40,11 @@
3540
use Illuminate\Support\HtmlString;
3641
use Illuminate\Support\Js;
3742
use Illuminate\Support\Str;
38-
use Illuminate\View\ComponentAttributeBag;
3943
use Livewire\Drawer\Utils;
4044

45+
use function Filament\Support\generate_icon_html;
46+
use function Filament\Support\generate_loading_indicator_html;
47+
4148
class Action extends ViewComponent implements Arrayable
4249
{
4350
use CanGenerateBadgeHtml;
@@ -653,6 +660,8 @@ public function call(array $parameters = []): mixed
653660
if ($this->shouldDeselectRecordsAfterCompletion()) {
654661
$this->getLivewire()->deselectAllTableRecords();
655662
}
663+
664+
$this->clearVisibilityCache();
656665
}
657666
}
658667

@@ -763,6 +772,14 @@ public function toHtml(): string
763772
return $this->toEmbeddedHtml();
764773
}
765774

775+
if ($this->canRenderOptimizedLink()) {
776+
return $this->toOptimizedLinkHtml();
777+
}
778+
779+
if ($this->canRenderOptimizedGrouped()) {
780+
return $this->toOptimizedGroupedHtml();
781+
}
782+
766783
return match ($this->getView()) {
767784
static::BADGE_VIEW => $this->toBadgeHtml(),
768785
static::BUTTON_VIEW => $this->toButtonHtml(),
@@ -773,14 +790,200 @@ public function toHtml(): string
773790
};
774791
}
775792

793+
protected function canRenderOptimizedLink(): bool
794+
{
795+
return ($this->getView() === static::LINK_VIEW)
796+
&& $this->hasSmallSizeDefault()
797+
&& ! $this->hasBadge()
798+
&& ! $this->hasTooltip()
799+
&& ! $this->hasIconPosition()
800+
&& ! $this->hasIconSize()
801+
&& ! $this->hasKeyBindings()
802+
&& ! $this->hasLabeledFromBreakpoint()
803+
&& ! $this->hasOutlined()
804+
&& ! $this->hasLabelHidden()
805+
&& ! $this->hasDisabled()
806+
&& ! $this->hasShouldOpenUrlInNewTab()
807+
&& ! $this->hasShouldPostToUrl()
808+
&& ! $this->hasClose()
809+
&& ! $this->hasCanAccessSelectedRecords()
810+
&& ! $this->hasCustomModalPresence()
811+
&& ! $this->hasAuthorization()
812+
&& ! $this->hasExtraAttributes()
813+
&& ! $this->hasMarkAsRead()
814+
&& ! $this->hasMarkAsUnread()
815+
&& ! $this->hasAlpineClickHandler()
816+
&& ! $this->hasCustomLivewireClickHandler()
817+
&& ! $this->hasLivewireTarget()
818+
&& ! $this->hasCanSubmitForm()
819+
&& ! $this->hasFormId()
820+
&& ! $this->hasTableIcon()
821+
&& ! $this->hasGroupedIcon();
822+
}
823+
824+
public function hasMarkAsRead(): bool
825+
{
826+
return $this->shouldMarkAsRead !== false;
827+
}
828+
829+
public function hasMarkAsUnread(): bool
830+
{
831+
return $this->shouldMarkAsUnread !== false;
832+
}
833+
834+
public function hasAlpineClickHandler(): bool
835+
{
836+
return $this->alpineClickHandler !== null;
837+
}
838+
839+
public function hasCustomLivewireClickHandler(): bool
840+
{
841+
return $this->isLivewireClickHandlerEnabled !== null;
842+
}
843+
844+
public function hasLivewireTarget(): bool
845+
{
846+
return $this->livewireTarget !== null;
847+
}
848+
849+
protected function toOptimizedLinkHtml(): string
850+
{
851+
$color = $this->getColor() ?? 'primary';
852+
853+
if (is_array($color)) {
854+
$classString = 'fi-ac-link-action fi-link fi-size-sm fi-color';
855+
$styleString = ' style="' . implode('; ', FilamentColor::getComponentCustomStyles(LinkComponent::class, $color)) . '"';
856+
} else {
857+
$colorClasses = implode(' ', FilamentColor::getComponentClasses(LinkComponent::class, $color));
858+
$classString = "fi-ac-link-action fi-link fi-size-sm {$colorClasses}";
859+
$styleString = '';
860+
}
861+
862+
$url = $this->getUrl();
863+
$icon = $this->getIcon();
864+
$label = e($this->getLabel());
865+
866+
if (filled($url)) {
867+
$iconHtml = $icon ? generate_icon_html($icon, size: IconSize::Small)?->toHtml() : '';
868+
$href = e($url);
869+
870+
return "<a href=\"{$href}\" class=\"{$classString}\"{$styleString}>{$iconHtml}{$label}</a>";
871+
}
872+
873+
$handler = $this->getLivewireClickHandler();
874+
875+
if (blank($handler)) {
876+
return "<span class=\"{$classString}\"{$styleString}>{$label}</span>";
877+
}
878+
879+
$iconHtml = $icon ? generate_icon_html(
880+
$icon,
881+
attributes: (new FilamentComponentAttributeBag([
882+
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => true,
883+
'wire:target' => $handler,
884+
])),
885+
size: IconSize::Small,
886+
)?->toHtml() : '';
887+
888+
$loadingHtml = generate_loading_indicator_html(
889+
(new FilamentComponentAttributeBag([
890+
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
891+
'wire:target' => $handler,
892+
])),
893+
size: IconSize::Small,
894+
)->toHtml();
895+
896+
// Match `ComponentAttributeBag::__toString()` attribute escaping (only `"` → `\"`).
897+
$handler = str_replace('"', '\\"', $handler);
898+
899+
return "<button type=\"button\" wire:loading.attr=\"disabled\" wire:click=\"{$handler}\" class=\"{$classString}\"{$styleString}>{$iconHtml}{$loadingHtml}{$label}</button>";
900+
}
901+
902+
protected function canRenderOptimizedGrouped(): bool
903+
{
904+
return ($this->getView() === static::GROUPED_VIEW)
905+
&& ! $this->hasBadge()
906+
&& ! $this->hasTooltip()
907+
&& ! $this->hasIconSize()
908+
&& ! $this->hasKeyBindings()
909+
&& ! $this->hasDisabled()
910+
&& ! $this->hasShouldOpenUrlInNewTab()
911+
&& ! $this->hasShouldPostToUrl()
912+
&& ! $this->hasClose()
913+
&& ! $this->hasCanAccessSelectedRecords()
914+
&& ! $this->hasCustomModalPresence()
915+
&& ! $this->hasAuthorization()
916+
&& ! $this->hasExtraAttributes()
917+
&& ! $this->hasMarkAsRead()
918+
&& ! $this->hasMarkAsUnread()
919+
&& ! $this->hasAlpineClickHandler()
920+
&& ! $this->hasCustomLivewireClickHandler()
921+
&& ! $this->hasLivewireTarget()
922+
&& ! $this->hasCanSubmitForm();
923+
}
924+
925+
protected function toOptimizedGroupedHtml(): string
926+
{
927+
$color = $this->getColor() ?? 'gray';
928+
929+
if (is_array($color)) {
930+
$classString = 'fi-dropdown-list-item fi-ac-grouped-action fi-color';
931+
$styleString = ' style="' . implode('; ', FilamentColor::getComponentCustomStyles(ItemComponent::class, $color)) . '"';
932+
} else {
933+
$colorClasses = implode(' ', FilamentColor::getComponentClasses(ItemComponent::class, $color));
934+
$classString = "fi-dropdown-list-item fi-ac-grouped-action {$colorClasses}";
935+
$styleString = '';
936+
}
937+
938+
$url = $this->getUrl();
939+
$icon = $this->getIcon(default: $this->getGroupedIcon());
940+
$label = e($this->getLabel());
941+
942+
if (filled($url)) {
943+
$iconHtml = $icon ? generate_icon_html(
944+
$icon,
945+
attributes: (new FilamentComponentAttributeBag)->color(IconComponent::class, $color),
946+
)?->toHtml() : '';
947+
$href = e($url);
948+
949+
return "<a href=\"{$href}\" class=\"{$classString}\"{$styleString}>{$iconHtml}<span class=\"fi-dropdown-list-item-label\">{$label}</span></a>";
950+
}
951+
952+
$handler = $this->getLivewireClickHandler();
953+
954+
if (blank($handler)) {
955+
return "<button type=\"button\" class=\"{$classString}\"{$styleString}><span class=\"fi-dropdown-list-item-label\">{$label}</span></button>";
956+
}
957+
958+
$iconHtml = $icon ? generate_icon_html(
959+
$icon,
960+
attributes: (new FilamentComponentAttributeBag([
961+
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => true,
962+
'wire:target' => $handler,
963+
]))->color(IconComponent::class, $color),
964+
)?->toHtml() : '';
965+
966+
$loadingHtml = generate_loading_indicator_html(
967+
(new FilamentComponentAttributeBag([
968+
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
969+
'wire:target' => $handler,
970+
])),
971+
)->toHtml();
972+
973+
// Match `ComponentAttributeBag::__toString()` attribute escaping (only `"` → `\"`).
974+
$handlerEscaped = str_replace('"', '\\"', $handler);
975+
976+
return "<button type=\"button\" wire:loading.attr=\"disabled\" wire:click=\"{$handlerEscaped}\" class=\"{$classString}\"{$styleString}>{$iconHtml}{$loadingHtml}<span class=\"fi-dropdown-list-item-label\">{$label}</span></button>";
977+
}
978+
776979
protected function toBadgeHtml(): string
777980
{
778981
$isDisabled = $this->isDisabled();
779982
$url = $this->getUrl();
780983
$shouldPostToUrl = $this->shouldPostToUrl();
781984

782985
return $this->generateBadgeHtml(
783-
attributes: (new ComponentAttributeBag([
986+
attributes: (new FilamentComponentAttributeBag([
784987
'action' => $shouldPostToUrl ? $url : null,
785988
'method' => $shouldPostToUrl ? 'post' : null,
786989
'wire:click' => $this->getLivewireClickHandler(),
@@ -814,7 +1017,7 @@ protected function toButtonHtml(): string
8141017
$shouldPostToUrl = $this->shouldPostToUrl();
8151018

8161019
return $this->generateButtonHtml(
817-
attributes: (new ComponentAttributeBag([
1020+
attributes: (new FilamentComponentAttributeBag([
8181021
'action' => $shouldPostToUrl ? $url : null,
8191022
'method' => $shouldPostToUrl ? 'post' : null,
8201023
'wire:click' => $this->getLivewireClickHandler(),
@@ -853,7 +1056,7 @@ protected function toGroupedHtml(): string
8531056
$shouldPostToUrl = $this->shouldPostToUrl();
8541057

8551058
return $this->generateDropdownItemHtml(
856-
attributes: (new ComponentAttributeBag([
1059+
attributes: (new FilamentComponentAttributeBag([
8571060
'action' => $shouldPostToUrl ? $url : null,
8581061
'method' => $shouldPostToUrl ? 'post' : null,
8591062
'wire:click' => $this->getLivewireClickHandler(),
@@ -886,7 +1089,7 @@ protected function toIconButtonHtml(): string
8861089
$shouldPostToUrl = $this->shouldPostToUrl();
8871090

8881091
return $this->generateIconButtonHtml(
889-
attributes: (new ComponentAttributeBag([
1092+
attributes: (new FilamentComponentAttributeBag([
8901093
'action' => $shouldPostToUrl ? $url : null,
8911094
'method' => $shouldPostToUrl ? 'post' : null,
8921095
'wire:click' => $this->getLivewireClickHandler(),
@@ -921,7 +1124,7 @@ protected function toLinkHtml(): string
9211124
$shouldPostToUrl = $this->shouldPostToUrl();
9221125

9231126
return $this->generateLinkHtml(
924-
attributes: (new ComponentAttributeBag([
1127+
attributes: (new FilamentComponentAttributeBag([
9251128
'action' => $shouldPostToUrl ? $url : null,
9261129
'method' => $shouldPostToUrl ? 'post' : null,
9271130
'wire:click' => $this->getLivewireClickHandler(),

0 commit comments

Comments
 (0)