Merge remote-tracking branch 'origin/3.x' into 3.x

This commit is contained in:
Brett Bailey
2025-06-11 16:19:51 +10:00
27 changed files with 192 additions and 54 deletions
@@ -27,6 +27,10 @@
:wire:key="$action ? $this->getId() . '.actions.' . $action->getName() . '.modal' : null"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedActions.length) open()"
x-on:modal-closed.stop="
if (!$event.detail?.id?.startsWith('{{ $this->getId() }}-')) {
return
}
const mountedActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedActionShouldOpenModal(mountedAction: $action)) }}
if (! mountedActionShouldOpenModal) {
@@ -89,6 +93,10 @@
:wire:key="$action ? $this->getId() . '.table.actions.' . $action->getName() . '.modal' : null"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedTableActions.length) open()"
x-on:modal-closed.stop="
if (!$event.detail?.id?.startsWith('{{ $this->getId() }}-')) {
return
}
const mountedTableActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedTableActionShouldOpenModal(mountedAction: $action)) }}
if (! mountedTableActionShouldOpenModal) {
@@ -145,6 +153,10 @@
:wire:key="$action ? $this->getId() . '.table.bulk-actions.' . $action->getName() . '.modal' : null"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedTableBulkAction) open()"
x-on:modal-closed.stop="
if (!$event.detail?.id?.startsWith('{{ $this->getId() }}-')) {
return
}
const mountedTableBulkActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedTableBulkActionShouldOpenModal(mountedBulkAction: $action)) }}
if (! mountedTableBulkActionShouldOpenModal) {
@@ -207,6 +219,10 @@
:wire:key="$action ? $this->getId() . '.infolist.actions.' . $action->getName() . '.modal' : null"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedInfolistActions.length) open()"
x-on:modal-closed.stop="
if (!$event.detail?.id?.startsWith('{{ $this->getId() }}-')) {
return
}
const mountedInfolistActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedInfolistActionShouldOpenModal(mountedAction: $action)) }}
if (! mountedInfolistActionShouldOpenModal) {
@@ -268,6 +284,10 @@
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->getId() . '.' . $action->getComponent()->getStatePath() . '.actions.' . $action->getName() . '.modal' : null"
x-on:modal-closed.stop="
if (!$event.detail?.id?.startsWith('{{ $this->getId() }}-')) {
return
}
const mountedFormComponentActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedFormComponentActionShouldOpenModal()) }}
if (mountedFormComponentActionShouldOpenModal) {
+1 -1
View File
@@ -560,7 +560,7 @@ Group::make()
Please note that if you are saving the data to a `BelongsTo` relationship, then the foreign key column in your database must be `nullable()`. This is because Filament saves the form first, before saving the relationship. Since the form is saved first, the foreign ID does not exist yet, so it must be nullable. Immediately after the form is saved, Filament saves the relationship, which will then fill in the foreign ID and save it again.
It is worth noting that if you have an observer on your form model, then you may need to adapt it to ensure that it does not depend on the relationship existing when it it created. For example, if you have an observer that sends an email to a related record when a form is created, you may need to switch to using a different hook that runs after the relationship is attached, like `updated()`.
It is worth noting that if you have an observer on your form model, then you may need to adapt it to ensure that it does not depend on the relationship existing when it is created. For example, if you have an observer that sends an email to a related record when a form is created, you may need to switch to using a different hook that runs after the relationship is attached, like `updated()`.
### Conditionally saving data to a relationship
@@ -111,7 +111,7 @@ return [
'actions' => [
'deselect_all' => [
'label' => 'Poista valinta kaikista',
'label' => 'Poista valinnat',
],
'select_all' => [
@@ -408,6 +408,8 @@ return [
'create_option' => [
'label' => 'Uusi',
'modal' => [
'heading' => 'Uusi',
@@ -430,6 +432,8 @@ return [
'edit_option' => [
'label' => 'Muokkaa',
'modal' => [
'heading' => 'Muokkaa',
@@ -455,7 +459,7 @@ return [
'loading_message' => 'Ladataan...',
'max_items_message' => 'Vain :count voi valita.',
'max_items_message' => 'Voit valita enintään :count.',
'no_search_results_message' => 'Haku ei löytänyt tuloksia.',
@@ -384,7 +384,7 @@ return [
'actions' => [
'create_option' => [
'label' => 'Créer',
'modal' => [
'heading' => 'Créer',
@@ -406,7 +406,7 @@ return [
],
'edit_option' => [
'label' => 'Modifier',
'modal' => [
'heading' => 'Modifier',
@@ -89,7 +89,7 @@ trait HasState
return $this;
}
public function callAfterStateUpdated(): static
public function callAfterStateUpdated(bool $shouldBubbleToParents = true): static
{
foreach ($this->afterStateUpdated as $callback) {
$runId = spl_object_id($callback) . md5(json_encode($this->getState()));
@@ -103,6 +103,10 @@ trait HasState
store($this)->push('executedAfterStateUpdatedCallbacks', value: $runId, iKey: $runId);
}
if ($shouldBubbleToParents) {
$this->getContainer()->getParentComponent()?->callAfterStateUpdated();
}
return $this;
}
@@ -189,7 +193,7 @@ trait HasState
// we need to dehydrate the child component containers while
// informing them that they are not dehydrated, so that their
// child components get removed from the state.
foreach ($this->getChildComponentContainers() as $container) {
foreach ($this->getChildComponentContainers(withHidden: true) as $container) {
$container->dehydrateState($state, isDehydrated: false);
}
+11 -3
View File
@@ -778,6 +778,14 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
]) ?? $relationshipQuery;
}
$baseRelationshipQuery = $relationshipQuery->getQuery();
if (isset($baseRelationshipQuery->limit)) {
$component->optionsLimit($baseRelationshipQuery->limit);
} elseif ($component->isSearchable() && filled($component->getSearchColumns())) {
$relationshipQuery->limit($component->getOptionsLimit());
}
$qualifiedRelatedKeyName = $component->getQualifiedRelatedKeyNameForRelationship($relationship);
if ($component->hasOptionLabelFromRecordUsingCallback()) {
@@ -1081,7 +1089,7 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
$query->where(function (Builder $query) use ($databaseConnection, $isForcedCaseInsensitive, $search): Builder {
$isFirst = true;
foreach ($this->getSearchColumns() as $searchColumn) {
foreach ($this->getSearchColumns() ?? [] as $searchColumn) {
$whereClause = $isFirst ? 'where' : 'orWhere';
$query->{$whereClause}(
@@ -1207,8 +1215,8 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
public function hasDynamicSearchResults(): bool
{
if ($this->hasRelationship() && empty($this->searchColumns)) {
return ! $this->isPreloaded();
if ($this->hasRelationship() && blank($this->getSearchColumns())) {
return false;
}
return $this->getSearchResultsUsing instanceof Closure;
+2 -2
View File
@@ -27,13 +27,13 @@ trait HasState
{
foreach ($this->getComponents(withHidden: true) as $component) {
if ($component->getStatePath() === $path) {
$component->callAfterStateUpdated();
$component->callAfterStateUpdated(shouldBubbleToParents: false);
return true;
}
if (str($path)->startsWith("{$component->getStatePath()}.")) {
$component->callAfterStateUpdated();
$component->callAfterStateUpdated(shouldBubbleToParents: false);
}
foreach ($component->getChildComponentContainers() as $container) {
@@ -116,10 +116,6 @@
@if ($hasLimitedRemainingText && (! $isLimitedRemainingTextSeparate) && $isCircular)
<div
style="
@if ($height) height: {{ $height }}; @endif
@if ($width) width: {{ $width }}; @endif
"
@class([
'flex items-center justify-center bg-gray-100 font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-400',
'rounded-full' => $isCircular,
+33
View File
@@ -215,6 +215,39 @@ class Dashboard extends BaseDashboard
Handling data from the filter action is the same as handling data from the filters header form, except that the data is validated before being passed to the widget. The `InteractsWithPageFilters` trait still applies.
### Persisting widget filters in the user's session
By default, the dashboard filters applied will persist in the user's session between page loads. To disable this, override the `$persistsFiltersInSession` property in the dashboard page class:
```php
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
protected bool $persistsFiltersInSession = false;
}
```
Alternatively, override the `persistsFiltersInSession()` method in the dashboard page class:
```php
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
public function persistsFiltersInSession(): bool
{
return false;
}
}
```
## Disabling the default widgets
By default, two widgets are displayed on the dashboard. These widgets can be disabled by updating the `widgets()` array of the [configuration](configuration):
@@ -4,7 +4,7 @@ return [
'field' => [
'label' => 'Globaali haku',
'placeholder' => 'Etsi',
'placeholder' => 'Hae',
],
'no_results_message' => 'Hakutuloksia ei löytynyt.',
@@ -9,7 +9,7 @@ return [
'actions' => [
'resend_notification' => [
'label' => 'Lähetä uudelleen ',
'label' => 'Lähetä uudelleen',
],
],
@@ -32,6 +32,10 @@ return [
'notifications' => [
'sent' => [
'body' => 'Jos tiliä ei ole olemassa, et vastaanota sähköpostia.',
],
'throttled' => [
'title' => 'Liian monta pyyntöä',
'body' => 'Yritä uudelleen :seconds sekunnin kuluttua.',
@@ -32,6 +32,10 @@ return [
'notifications' => [
'sent' => [
'body' => 'Se sua conta não existir, você não receberá o e-mail.',
],
'throttled' => [
'title' => 'Muitas solicitações',
'body' => 'Por favor tente novamente em :seconds segundos.',
@@ -15,7 +15,7 @@ return [
],
'messages' => [
'notification_not_received' => 'Inte fått mejlet vi skickade?',
'notification_not_received' => 'Inte fått meddelandet vi skickade?',
'notification_sent' => 'Vi skickade ett meddelande till :email med instruktioner på hur du verifierar din mejladress.',
],
@@ -23,7 +23,7 @@ return [
'actions' => [
'request' => [
'label' => 'Skicka mejlmeddelande',
'label' => 'Skicka meddelande',
],
],
@@ -32,6 +32,10 @@ return [
'notifications' => [
'sent' => [
'body' => 'Om ditt konto inte finns, kommer du inte att få något meddelande.',
],
'throttled' => [
'title' => 'För många förfrågningar',
'body' => 'Vänligen försök igen om :seconds sekunder.',
@@ -12,7 +12,7 @@
])
>
<head>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::HEAD_START, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::HEAD_START, scopes: $livewire?->getRenderHookScopes()) }}
<meta charset="utf-8" />
<meta name="csrf-token" content="{{ csrf_token() }}" />
@@ -31,7 +31,7 @@
{{ filled($title) ? "{$title} - " : null }} {{ $brandName }}
</title>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_BEFORE, scopes: $livewire?->getRenderHookScopes()) }}
<style>
[x-cloak=''],
@@ -69,7 +69,7 @@
@stack('styles')
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_AFTER, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_AFTER, scopes: $livewire?->getRenderHookScopes()) }}
@if (! filament()->hasDarkMode())
<script>
@@ -100,7 +100,7 @@
</script>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::HEAD_END, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::HEAD_END, scopes: $livewire?->getRenderHookScopes()) }}
</head>
<body
@@ -112,13 +112,13 @@
'min-h-screen bg-gray-50 font-normal text-gray-950 antialiased dark:bg-gray-950 dark:text-white',
]) }}
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::BODY_START, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::BODY_START, scopes: $livewire?->getRenderHookScopes()) }}
{{ $slot }}
@livewire(Filament\Livewire\Notifications::class)
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SCRIPTS_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SCRIPTS_BEFORE, scopes: $livewire?->getRenderHookScopes()) }}
@filamentScripts(withCore: true)
@@ -138,8 +138,8 @@
@stack('scripts')
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SCRIPTS_AFTER, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SCRIPTS_AFTER, scopes: $livewire?->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::BODY_END, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::BODY_END, scopes: $livewire?->getRenderHookScopes()) }}
</body>
</html>
@@ -2,6 +2,7 @@
use Filament\Support\Enums\MaxWidth;
$navigation = filament()->getNavigation();
$livewire ??= null;
@endphp
<x-filament-panels::layout.base :livewire="$livewire">
@@ -34,11 +35,11 @@
])
>
@if (filament()->hasTopbar())
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_BEFORE, scopes: $livewire?->getRenderHookScopes()) }}
<x-filament-panels::topbar :navigation="$navigation" />
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_AFTER, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_AFTER, scopes: $livewire?->getRenderHookScopes()) }}
@endif
<main
@@ -70,14 +71,14 @@
},
])
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::CONTENT_START, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::CONTENT_START, scopes: $livewire?->getRenderHookScopes()) }}
{{ $slot }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::CONTENT_END, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::CONTENT_END, scopes: $livewire?->getRenderHookScopes()) }}
</main>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire?->getRenderHookScopes()) }}
</div>
@if (filament()->hasNavigation())
@@ -1,5 +1,7 @@
@php
use Filament\Support\Enums\MaxWidth;
$livewire ??= null;
@endphp
<x-filament-panels::layout.base :livewire="$livewire">
@@ -60,6 +62,6 @@
</main>
</div>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire?->getRenderHookScopes()) }}
</div>
</x-filament-panels::layout.base>
@@ -0,0 +1,9 @@
<?php
return [
'messages' => [
'copied' => '已複製',
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'close' => [
'label' => '關閉',
],
],
];
@@ -9,17 +9,31 @@ return [
'fields' => [
'records_per_page' => [
'label' => '每頁顯示',
'options' => [
'all' => '全部',
],
],
],
'actions' => [
'first' => [
'label' => '第一頁',
],
'go_to_page' => [
'label' => '前往第 :page 頁',
],
'last' => [
'label' => '最後一頁',
],
'next' => [
'label' => '下一頁',
],
@@ -72,7 +72,7 @@
this.isOpen = false
this.$refs.modalContainer.dispatchEvent(
new CustomEvent('modal-closed', { id: '{{ $id }}' }),
new CustomEvent('modal-closed', { detail: { id: '{{ $id }}' } }),
)
},
@@ -85,7 +85,7 @@
@endif
this.$refs.modalContainer.dispatchEvent(
new CustomEvent('modal-opened', { id: '{{ $id }}' }),
new CustomEvent('modal-opened', { detail: { id: '{{ $id }}' } }),
)
})
},
+10 -10
View File
@@ -42,9 +42,9 @@ return [
],
'search' => [
'label' => 'Etsi',
'placeholder' => 'Etsi',
'indicator' => 'Etsi',
'label' => 'Haku',
'placeholder' => 'Hae',
'indicator' => 'Haku',
],
],
@@ -118,7 +118,7 @@ return [
'actions' => [
'apply' => [
'label' => 'Käytä suodattimet',
'label' => 'Aseta suodattimet',
],
'remove' => [
@@ -126,12 +126,12 @@ return [
],
'remove_all' => [
'label' => 'Poista suodattimet',
'tooltip' => 'Poista suodattimet',
'label' => 'Tyhjennä suodattimet',
'tooltip' => 'Tyhjennä suodattimet',
],
'reset' => [
'label' => 'Tyhjennä suodattimet',
'label' => 'Palauta',
],
],
@@ -173,7 +173,7 @@ return [
'direction' => [
'label' => 'Ryhmittelyn suunta',
'label' => 'Ryhmitys',
'options' => [
'asc' => 'Nousevasti',
@@ -199,7 +199,7 @@ return [
],
'deselect_all' => [
'label' => 'Poista valinta kaikista',
'label' => 'Poista valinnat',
],
],
@@ -216,7 +216,7 @@ return [
'direction' => [
'label' => 'Järjestyksen suunta',
'label' => 'Järjestys',
'options' => [
'asc' => 'Nousevasti',
@@ -102,10 +102,6 @@
@if ($hasLimitedRemainingText && (! $isLimitedRemainingTextSeparate) && $isCircular)
<div
style="
@if ($height) height: {{ $height }}; @endif
@if ($width) width: {{ $width }}; @endif
"
@class([
'flex items-center justify-center bg-gray-100 font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-400',
'rounded-full' => $isCircular,
@@ -23,14 +23,18 @@
@endif
{{ $attributes->class(['fi-ta-header-cell px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6']) }}
>
<{{ $sortable ? 'button' : 'span' }}
<span
@if ($sortable)
aria-label="{{ trim(strip_tags($slot)) }}"
type="button"
role="button"
tabindex="0"
wire:click="sortTable('{{ $name }}')"
x-on:keydown.enter.prevent.stop="$wire.sortTable('{{ $name }}')"
x-on:keydown.space.prevent.stop="$wire.sortTable('{{ $name }}')"
@endif
@class([
'group flex w-full items-center gap-x-1',
'cursor-pointer' => $sortable,
'whitespace-nowrap' => ! $wrap,
'whitespace-normal' => $wrap,
match ($alignment) {
@@ -67,5 +71,5 @@
])
/>
@endif
</{{ $sortable ? 'button' : 'span' }}>
</span>
</th>
@@ -640,7 +640,7 @@
@if ($recordHasActions)
<x-filament-tables::actions
:actions="$actions"
:alignment="(! $contentGrid) ? 'start md:end' : Alignment::Start"
:alignment="(! $contentGrid) ? 'start md:end' : $actionsAlignment ?? Alignment::Start"
:record="$record"
wrap="-sm"
:class="$recordActionsClasses"
+22
View File
@@ -599,6 +599,28 @@ test('hidden components are not excluded from state dehydration if there is anot
->dehydrateState()->data->not()->toBe([]);
});
test('components in a hidden parent component with a grandparent component with a state path are excluded from state dehydration', function () {
$container = ComponentContainer::make(Livewire::make())
->statePath('data')
->components([
(new Component)
->statePath('data')
->schema([
(new Component)
->schema([
(new Component)
->statePath(Str::random())
->default(Str::random()),
])
->hidden(),
]),
])
->fill();
expect($container)
->dehydrateState()->toBe(['data' => ['data' => []]]);
});
test('disabled components are excluded from state dehydration', function () {
$container = ComponentContainer::make(Livewire::make())
->statePath('data')