diff --git a/packages/panels/resources/lang/de/widgets/empty-state.php b/packages/panels/resources/lang/de/widgets/empty-state.php new file mode 100644 index 00000000000..8f315f150ae --- /dev/null +++ b/packages/panels/resources/lang/de/widgets/empty-state.php @@ -0,0 +1,7 @@ + 'Keine Daten vorhanden', + +]; diff --git a/packages/panels/resources/lang/en/widgets/empty-state.php b/packages/panels/resources/lang/en/widgets/empty-state.php new file mode 100644 index 00000000000..1eb9a70c6ab --- /dev/null +++ b/packages/panels/resources/lang/en/widgets/empty-state.php @@ -0,0 +1,7 @@ + 'Nothing to display', + +]; diff --git a/packages/support/docs/03-icons.md b/packages/support/docs/03-icons.md index a8f260b5ab8..298bc72ad26 100644 --- a/packages/support/docs/03-icons.md +++ b/packages/support/docs/03-icons.md @@ -97,6 +97,7 @@ Alternatively, you may pass an SVG element into the component's slot instead of - `panels::widgets.account.logout-button` - Button in the account widget to log out - `panels::widgets.filament-info.open-documentation-button` - Button to open the documentation from the Filament info widget - `panels::widgets.filament-info.open-github-button` - Button to open GitHub from the Filament info widget +- `panels::widgets.empty-state` - Empty state icon for chart widgets ### Form Builder icon aliases diff --git a/packages/widgets/docs/03-charts.md b/packages/widgets/docs/03-charts.md index 3e598d3189d..4968f7e4a85 100644 --- a/packages/widgets/docs/03-charts.md +++ b/packages/widgets/docs/03-charts.md @@ -319,3 +319,72 @@ FilamentAsset::register([ ``` You can find out more about [asset registration](../support/assets), and even [register assets for a specific panel](../panels/configuration#registering-assets-for-a-panel). + +--- + +## Empty state + +The chart's "empty state" is rendered when there are is no data to display. +By default, the chart will not display this by itself. You have to figure out when to display it and simply call `$this->empty()` from another method of a Chart Widget. +Usually, you would call this method from the `getData()` method, when there is no data to display. + +```php +protected function getData(): array +{ + ... + if ($noData) { + $this->empty(); + } +} +``` + +## Setting the empty state heading + +To customize the heading of the empty state, set `$emptyStateHeading`: + +```php +protected string | Htmlable | Closure | null $emptyStateHeading = 'No data to display :('; +``` + +## Setting the empty state description + +To customize the description of the empty state, set `$emptyStateDescription`: + +```php +protected string | Htmlable | Closure | null $emptyStateDescription = 'You will see data here once you added some more of it.'; +``` + +## Setting the empty state icon + +To customize the [icon](https://blade-ui-kit.com/blade-icons?set=1#search) of the empty state, set `$emptyStateIcon`: + +```php +protected string | Closure | null $emptyStateIcon = 'heroicon-o-chart-pie'; +``` + +## Adding empty state actions + +You can add Actions or Action Groups to the empty state to prompt users to take action. Simply override the `getEmptyStateActions()` method: + +```php +use Filament\Actions\Action; + +public function getEmptyStateActions(): array +{ + return [ + Action::make('create') + ->label('Create post') + ->url(route('posts.create')) + ->icon('heroicon-m-plus') + ->button(), + ]; +} +``` + +## Using a custom empty state view + +You may use a completely custom empty state view by passing it to the `$emptyState`: + +```php +protected View | Htmlable | Closure | null $emptyState = view('empty-state'); +``` diff --git a/packages/widgets/resources/views/chart-widget.blade.php b/packages/widgets/resources/views/chart-widget.blade.php index 83a2473239d..faa91833b2e 100644 --- a/packages/widgets/resources/views/chart-widget.blade.php +++ b/packages/widgets/resources/views/chart-widget.blade.php @@ -5,6 +5,7 @@ $heading = $this->getHeading(); $description = $this->getDescription(); $filters = $this->getFilters(); + $empty = $this->isEmpty(); @endphp @@ -35,79 +36,94 @@ class="w-max sm:-my-2" wire:poll.{{ $pollingInterval }}="updateChartData" @endif > -
getEmptyState()) + {{ $emptyState }} @else - ax-load +
+ +
@endif - ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('chart', 'filament/widgets') }}" - wire:ignore - x-data="chart({ - cachedData: @js($this->getCachedData()), - options: @js($this->getOptions()), - type: @js($this->getType()), - })" - x-ignore - @class([ - match ($color) { - 'gray' => null, - default => 'fi-color-custom', - }, - is_string($color) ? "fi-color-{$color}" : null, - ]) - > - getMaxHeight()) - style="max-height: {{ $maxHeight }}" + @else +
- - 'text-gray-100 dark:text-gray-800', - default => 'text-custom-50 dark:text-custom-400/10', + 'gray' => null, + default => 'fi-color-custom', }, + is_string($color) ? "fi-color-{$color}" : null, ]) - @style([ - \Filament\Support\get_color_css_variables( - $color, - shades: [50, 400], - alias: 'widgets::chart-widget.background', - ) => $color !== 'gray', - ]) - > + > + getMaxHeight()) + style="max-height: {{ $maxHeight }}" + @endif + > - 'text-gray-400', - default => 'text-custom-500 dark:text-custom-400', - }, - ]) - @style([ - \Filament\Support\get_color_css_variables( - $color, - shades: [400, 500], - alias: 'widgets::chart-widget.border', - ) => $color !== 'gray', - ]) - > + 'text-gray-100 dark:text-gray-800', + default => 'text-custom-50 dark:text-custom-400/10', + }, + ]) + @style([ + \Filament\Support\get_color_css_variables( + $color, + shades: [50, 400], + alias: 'widgets::chart-widget.background', + ) => $color !== 'gray', + ]) + > - + 'text-gray-400', + default => 'text-custom-500 dark:text-custom-400', + }, + ]) + @style([ + \Filament\Support\get_color_css_variables( + $color, + shades: [400, 500], + alias: 'widgets::chart-widget.border', + ) => $color !== 'gray', + ]) + > - -
+ + + +
+ @endif
diff --git a/packages/widgets/resources/views/components/empty-state/description.blade.php b/packages/widgets/resources/views/components/empty-state/description.blade.php new file mode 100644 index 00000000000..b839821f20b --- /dev/null +++ b/packages/widgets/resources/views/components/empty-state/description.blade.php @@ -0,0 +1,5 @@ +

class(['fi-wi-empty-state-description text-sm text-gray-500 dark:text-gray-400']) }} +> + {{ $slot }} +

diff --git a/packages/widgets/resources/views/components/empty-state/heading.blade.php b/packages/widgets/resources/views/components/empty-state/heading.blade.php new file mode 100644 index 00000000000..b4bc24312c2 --- /dev/null +++ b/packages/widgets/resources/views/components/empty-state/heading.blade.php @@ -0,0 +1,5 @@ +

class(['fi-wi-empty-state-heading text-base font-semibold leading-6 text-gray-950 dark:text-white']) }} +> + {{ $slot }} +

diff --git a/packages/widgets/resources/views/components/empty-state/index.blade.php b/packages/widgets/resources/views/components/empty-state/index.blade.php new file mode 100644 index 00000000000..bcde43c88a8 --- /dev/null +++ b/packages/widgets/resources/views/components/empty-state/index.blade.php @@ -0,0 +1,46 @@ +@php + use Filament\Support\Enums\Alignment; +@endphp + +@props([ + 'actions' => [], + 'description' => null, + 'heading', + 'icon', +]) + +
class(['fi-wi-empty-state px-6 py-12']) }} +> +
+
+ +
+ + + {{ $heading }} + + + @if ($description) + + {{ $description }} + + @endif + + @if ($actions) + + @endif +
+
diff --git a/packages/widgets/src/ChartWidget.php b/packages/widgets/src/ChartWidget.php index 5b19f0012c5..72f245d85c0 100644 --- a/packages/widgets/src/ChartWidget.php +++ b/packages/widgets/src/ChartWidget.php @@ -9,6 +9,7 @@ abstract class ChartWidget extends Widget { use Concerns\CanPoll; + use Concerns\HasEmptyState; /** * @var array | null diff --git a/packages/widgets/src/Concerns/HasEmptyState.php b/packages/widgets/src/Concerns/HasEmptyState.php new file mode 100644 index 00000000000..635fbb6500a --- /dev/null +++ b/packages/widgets/src/Concerns/HasEmptyState.php @@ -0,0 +1,66 @@ +evaluate($this->emptyState); + } + + /** + * @return array + */ + public function getEmptyStateActions(): array + { + return []; + } + + public function getEmptyStateDescription(): string | Htmlable | null + { + return $this->evaluate($this->emptyStateDescription); + } + + public function getEmptyStateHeading(): string | Htmlable + { + return $this->evaluate($this->emptyStateHeading) ?? __('filament-panels::widgets/empty-state.heading'); + } + + public function getEmptyStateIcon(): string + { + return $this->evaluate($this->emptyStateIcon) + ?? FilamentIcon::resolve('panels::widgets.empty-state') + ?? 'heroicon-o-x-mark'; + } + + public function empty(bool $empty = true): void + { + $this->empty = $empty; + } + + public function isEmpty(): bool + { + return $this->empty; + } +}