🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class BarChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'bar';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class BubbleChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'bubble';
}
}
@@ -0,0 +1,134 @@
<?php
namespace Filament\Widgets;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Support\RawJs;
use Illuminate\Contracts\Support\Htmlable;
use Livewire\Attributes\Locked;
abstract class ChartWidget extends Widget implements HasSchemas
{
use Concerns\CanPoll;
use InteractsWithSchemas;
/**
* @var array<string, mixed> | null
*/
protected ?array $cachedData = null;
#[Locked]
public ?string $dataChecksum = null;
public ?string $filter = null;
protected string $color = 'primary';
protected ?string $heading = null;
protected ?string $description = null;
protected ?string $maxHeight = null;
/**
* @var array<string, mixed> | null
*/
protected ?array $options = null;
protected bool $isCollapsible = false;
/**
* @var view-string
*/
protected string $view = 'filament-widgets::chart-widget';
public function mount(): void
{
if (method_exists($this, 'getFiltersSchema')) {
$this->getFiltersSchema()->fill();
}
$this->dataChecksum = $this->generateDataChecksum();
}
abstract protected function getType(): string;
protected function generateDataChecksum(): string
{
return md5(json_encode($this->getCachedData()));
}
/**
* @return array<string, mixed>
*/
protected function getCachedData(): array
{
return $this->cachedData ??= $this->getData();
}
/**
* @return array<string, mixed>
*/
protected function getData(): array
{
return [];
}
/**
* @return array<scalar, scalar> | null
*/
protected function getFilters(): ?array
{
return null;
}
public function getHeading(): string | Htmlable | null
{
return $this->heading;
}
public function getDescription(): string | Htmlable | null
{
return $this->description;
}
protected function getMaxHeight(): ?string
{
return $this->maxHeight;
}
/**
* @return array<string, mixed> | RawJs | null
*/
protected function getOptions(): array | RawJs | null
{
return $this->options;
}
public function updateChartData(): void
{
$newDataChecksum = $this->generateDataChecksum();
if ($newDataChecksum !== $this->dataChecksum) {
$this->dataChecksum = $newDataChecksum;
$this->dispatch('updateChartData', data: $this->getCachedData());
}
}
public function rendering(): void
{
$this->updateChartData();
}
public function getColor(): string
{
return $this->color;
}
public function isCollapsible(): bool
{
return $this->isCollapsible;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Filament\Widgets\ChartWidget\Concerns;
use Filament\Actions\Action;
use Filament\Schemas\Schema;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Filament\Widgets\View\WidgetsIconAlias;
trait HasFiltersSchema /** @phpstan-ignore trait.unused */
{
public ?array $filters = [];
public function filtersSchema(Schema $schema): Schema
{
return $schema;
}
public function getFiltersTriggerAction(): Action
{
return Action::make('filter')
->label(__('filament-widgets::chart.actions.filter.label'))
->iconButton()
->icon(FilamentIcon::resolve(WidgetsIconAlias::CHART_WIDGET_FILTER) ?? Heroicon::Funnel)
->color('gray')
->livewireClickHandlerEnabled(false);
}
public function getFiltersSchema(): Schema
{
if ((! $this->isCachingSchemas) && $this->hasCachedSchema('filtersSchema')) {
return $this->getSchema('filtersSchema');
}
return $this->filtersSchema($this->makeSchema()
->statePath('filters')
->live());
}
}
@@ -0,0 +1,114 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Widgets\ChartWidget;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\Property;
class ChartWidgetClassGenerator extends ClassGenerator
{
final public function __construct(
protected string $fqn,
protected string $type,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return ChartWidget::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addHeadingPropertyToClass($class);
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addGetDataMethodToClass($class);
$this->addGetTypeMethodToClass($class);
}
protected function addHeadingPropertyToClass(ClassType $class): void
{
$property = $class->addProperty(
'heading',
(string) str($this->getBasename())
->classBasename()
->kebab()
->replace('-', ' ')
->ucwords(),
)
->setProtected()
->setType('?string');
$this->configureHeadingProperty($property);
}
protected function configureHeadingProperty(Property $property): void {}
protected function addGetDataMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getData')
->setProtected()
->setReturnType('array')
->setBody(<<<'PHP'
return [
//
];
PHP);
$this->configureGetDataMethod($method);
}
protected function configureGetDataMethod(Method $method): void {}
protected function addGetTypeMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getType')
->setProtected()
->setReturnType('string')
->setBody(new Literal(<<<'PHP'
return ?;
PHP, [$this->getType()]));
$this->configureGetTypeMethod($method);
}
protected function configureGetTypeMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
public function getType(): string
{
return $this->type;
}
}
@@ -0,0 +1,61 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Support\Commands\FileGenerators\Concerns\CanGenerateViewProperty;
use Filament\Widgets\Widget;
use Nette\PhpGenerator\ClassType;
class CustomWidgetClassGenerator extends ClassGenerator
{
use CanGenerateViewProperty;
final public function __construct(
protected string $fqn,
protected string $view,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return Widget::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addViewPropertyToClass($class);
}
public function getFqn(): string
{
return $this->fqn;
}
public function getView(): string
{
return $this->view;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Method;
class StatsOverviewWidgetClassGenerator extends ClassGenerator
{
final public function __construct(
protected string $fqn,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
Stat::class,
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return StatsOverviewWidget::class;
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addGetStatsMethodToClass($class);
}
protected function addGetStatsMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getStats')
->setProtected()
->setReturnType('array')
->setBody(<<<'PHP'
return [
//
];
PHP);
$this->configureGetStatsMethod($method);
}
protected function configureGetStatsMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
}
@@ -0,0 +1,97 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\Concerns\CanReadModelSchemas;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Tables\Commands\FileGenerators\Concerns\CanGenerateModelTables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Method;
class TableWidgetClassGenerator extends ClassGenerator
{
use CanGenerateModelTables;
use CanReadModelSchemas;
/**
* @param class-string<Model> $modelFqn
*/
final public function __construct(
protected string $fqn,
protected string $modelFqn,
protected bool $isGenerated,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
Table::class,
...($this->hasPartialImports() ? ['Filament\Tables'] : []),
Builder::class,
$this->getModelFqn(),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return TableWidget::class;
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addTableMethodToClass($class);
}
protected function addTableMethodToClass(ClassType $class): void
{
$method = $class->addMethod('table')
->setPublic()
->setReturnType(Table::class)
->setBody($this->generateTableMethodBody($this->getModelFqn()));
$method->addParameter('table')
->setType(Table::class);
$this->configureTableMethod($method);
}
protected function configureTableMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
/**
* @return class-string<Model>
*/
public function getModelFqn(): string
{
return $this->modelFqn;
}
public function isGenerated(): bool
{
return $this->isGenerated;
}
}
@@ -0,0 +1,524 @@
<?php
namespace Filament\Widgets\Commands;
use Filament\Support\Commands\Concerns\CanAskForLivewireComponentLocation;
use Filament\Support\Commands\Concerns\CanAskForResource;
use Filament\Support\Commands\Concerns\CanAskForViewLocation;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Commands\Concerns\HasCluster;
use Filament\Support\Commands\Concerns\HasPanel;
use Filament\Support\Commands\Concerns\HasResourcesLocation;
use Filament\Support\Commands\Exceptions\FailureCommandOutput;
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
use Filament\Support\Facades\FilamentCli;
use Filament\Widgets\ChartWidget;
use Filament\Widgets\Commands\FileGenerators\ChartWidgetClassGenerator;
use Filament\Widgets\Commands\FileGenerators\CustomWidgetClassGenerator;
use Filament\Widgets\Commands\FileGenerators\StatsOverviewWidgetClassGenerator;
use Filament\Widgets\Commands\FileGenerators\TableWidgetClassGenerator;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\TableWidget;
use Filament\Widgets\Widget;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use ReflectionClass;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function Filament\Support\discover_app_classes;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\search;
use function Laravel\Prompts\select;
use function Laravel\Prompts\suggest;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:filament-widget', aliases: [
'filament:make-widget',
'filament:widget',
])]
class MakeWidgetCommand extends Command
{
use CanAskForLivewireComponentLocation;
use CanAskForResource;
use CanAskForViewLocation;
use CanCheckFileGenerationFlags;
use CanManipulateFiles;
use HasCluster;
use HasPanel;
use HasResourcesLocation;
protected $description = 'Create a new Filament widget class';
protected $name = 'make:filament-widget';
/**
* @var array<string>
*/
protected $aliases = [
'filament:make-widget',
'filament:widget',
];
/**
* @var class-string
*/
protected string $fqn;
protected string $fqnEnd;
protected ?string $view = null;
protected ?string $viewPath = null;
protected bool $hasResource;
/**
* @var ?class-string
*/
protected ?string $resourceFqn = null;
/**
* @var class-string<Widget> | null
*/
protected ?string $type = null;
protected string $widgetsNamespace;
protected string $widgetsDirectory;
/**
* @return array<InputArgument>
*/
protected function getArguments(): array
{
return [
new InputArgument(
name: 'name',
mode: InputArgument::OPTIONAL,
description: 'The name of the widget to generate, optionally prefixed with directories',
),
];
}
/**
* @return array<InputOption>
*/
protected function getOptions(): array
{
return [
new InputOption(
name: 'chart',
shortcut: 'C',
mode: InputOption::VALUE_NONE,
description: 'Create a chart widget',
),
new InputOption(
name: 'cluster',
shortcut: null,
mode: InputOption::VALUE_OPTIONAL,
description: 'The cluster that the resource belongs to',
),
new InputOption(
name: 'panel',
shortcut: null,
mode: InputOption::VALUE_REQUIRED,
description: 'The panel to create the widget in',
),
new InputOption(
name: 'resource',
shortcut: 'R',
mode: InputOption::VALUE_OPTIONAL,
description: 'The resource to create the widget in',
),
new InputOption(
name: 'resource-namespace',
shortcut: null,
mode: InputOption::VALUE_OPTIONAL,
description: 'The namespace of the resource class, such as [' . app()->getNamespace() . 'Filament\\Resources]',
),
new InputOption(
name: 'stats-overview',
shortcut: 'S',
mode: InputOption::VALUE_NONE,
description: 'Create a stats overview widget',
),
new InputOption(
name: 'table',
shortcut: 'T',
mode: InputOption::VALUE_NONE,
description: 'Create a table widget',
),
new InputOption(
name: 'force',
shortcut: 'F',
mode: InputOption::VALUE_NONE,
description: 'Overwrite the contents of the files if they already exist',
),
];
}
public function handle(): int
{
try {
$this->configureFqnEnd();
$this->configureType();
$this->configurePanel(
question: 'Which panel would you like to create this widget in?',
initialQuestion: 'Would you like to create this widget in a panel?',
);
$this->configureHasResource();
$this->configureCluster();
$this->configureResource();
$this->configureWidgetsLocation();
$this->configureLocation();
$this->createCustomWidget();
$this->createChartWidget();
$this->createStatsOverviewWidget();
$this->createTableWidget();
$this->createView();
} catch (FailureCommandOutput) {
return static::FAILURE;
}
$this->components->info("Filament widget [{$this->fqn}] created successfully.");
if (filled($this->resourceFqn)) {
$this->components->info("Make sure to register the widget in [{$this->resourceFqn}::getWidgets()], and add it to a page in the resource.");
} elseif ($this->panel && empty($this->panel->getWidgetNamespaces())) {
$this->components->info('Make sure to register the widget with [widgets()] or discover it with [discoverWidgets()] in the panel service provider.');
}
return static::SUCCESS;
}
protected function configureFqnEnd(): void
{
$this->fqnEnd = (string) str($this->argument('name') ?? text(
label: 'What is the widget name?',
placeholder: 'BlogPostsChart',
required: true,
))
->trim('/')
->trim('\\')
->trim(' ')
->studly()
->replace('/', '\\');
}
protected function configureType(): void
{
$this->type = match (true) {
boolval($this->option('chart')) => ChartWidget::class,
boolval($this->option('stats-overview')) => StatsOverviewWidget::class,
boolval($this->option('table')) => TableWidget::class,
default => null,
} ?? select(
label: 'Which type of widget would you like to create?',
options: [
Widget::class => 'Custom',
ChartWidget::class => 'Chart',
StatsOverviewWidget::class => 'Stats overview',
TableWidget::class => 'Table',
],
);
}
protected function configureHasResource(): void
{
if (! $this->panel) {
$this->hasResource = false;
return;
}
$this->hasResource = $this->option('resource') || confirm(
label: 'Would you like to create this widget in a resource?',
default: false,
);
}
protected function configureCluster(): void
{
if (! $this->hasResource) {
return;
}
$this->configureClusterFqn(
initialQuestion: 'Is the resource in a cluster?',
question: 'Which cluster is the resource in?',
);
if (blank($this->clusterFqn)) {
return;
}
$this->configureClusterResourcesLocation();
}
protected function configureResource(): void
{
if (! $this->hasResource) {
return;
}
$this->configureResourcesLocation(question: 'Which namespace would you like to search for resources in?');
$this->resourceFqn = $this->askForResource(
question: 'Which resource would you like to create this widget in?',
initialResource: $this->option('resource'),
);
$pluralResourceBasenameBeforeResource = (string) str($this->resourceFqn)
->classBasename()
->beforeLast('Resource')
->plural();
$resourceNamespacePartBeforeBasename = (string) str($this->resourceFqn)
->beforeLast('\\')
->classBasename();
if ($pluralResourceBasenameBeforeResource === $resourceNamespacePartBeforeBasename) {
$this->widgetsNamespace = (string) str($this->resourceFqn)
->beforeLast('\\')
->append('\\Widgets');
$this->widgetsDirectory = (string) str((new ReflectionClass($this->resourceFqn))->getFileName())
->beforeLast(DIRECTORY_SEPARATOR)
->append('/Widgets');
return;
}
$this->widgetsNamespace = "{$this->resourceFqn}\\Widgets";
$this->widgetsDirectory = (string) str((new ReflectionClass($this->resourceFqn))->getFileName())
->beforeLast('.')
->append('/Widgets');
}
protected function configureWidgetsLocation(): void
{
if (filled($this->resourceFqn)) {
return;
}
if (! $this->panel) {
[
$this->widgetsNamespace,
$this->widgetsDirectory,
] = $this->askForLivewireComponentLocation(
question: 'Where would you like to create the widget?',
);
return;
}
$directories = $this->panel->getWidgetDirectories();
$namespaces = $this->panel->getWidgetNamespaces();
foreach ($directories as $index => $directory) {
if (str($directory)->startsWith(base_path('vendor'))) {
unset($directories[$index]);
unset($namespaces[$index]);
}
}
if (count($namespaces) < 2) {
$this->widgetsNamespace = (Arr::first($namespaces) ?? app()->getNamespace() . 'Filament\\Widgets');
$this->widgetsDirectory = (Arr::first($directories) ?? app_path('Filament/Widgets/'));
return;
}
$keyedNamespaces = array_combine(
$namespaces,
$namespaces,
);
$this->widgetsNamespace = search(
label: 'Which namespace would you like to create this widget in?',
options: function (?string $search) use ($keyedNamespaces): array {
if (blank($search)) {
return $keyedNamespaces;
}
$search = str($search)->trim()->replace(['\\', '/'], '');
return array_filter($keyedNamespaces, fn (string $namespace): bool => str($namespace)->replace(['\\', '/'], '')->contains($search, ignoreCase: true));
},
);
$this->widgetsDirectory = $directories[array_search($this->widgetsNamespace, $namespaces)];
}
protected function configureLocation(): void
{
$this->fqn = $this->widgetsNamespace . '\\' . $this->fqnEnd;
if ($this->type === Widget::class) {
$componentLocations = FilamentCli::getLivewireComponentLocations();
$matchingComponentLocationNamespaces = collect($componentLocations)
->keys()
->filter(fn (string $namespace): bool => str($this->fqn)->startsWith($namespace));
[
$this->view,
$this->viewPath,
] = $this->askForViewLocation(
view: str($this->fqn)
->whenContains(
'Filament\\',
fn (Stringable $fqn) => $fqn->after('Filament\\')->prepend('Filament\\'),
fn (Stringable $fqn) => $fqn
->afterLast('\\Livewire\\')
->prepend('Livewire\\'),
)
->replace('\\', '/')
->explode('/')
->map(Str::kebab(...))
->implode('.'),
question: 'Where would you like to create the Blade view for the widget?',
defaultNamespace: (count($matchingComponentLocationNamespaces) === 1)
? $componentLocations[Arr::first($matchingComponentLocationNamespaces)]['viewNamespace'] ?? null
: null,
);
}
}
protected function createCustomWidget(): void
{
if ($this->type !== Widget::class) {
return;
}
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(CustomWidgetClassGenerator::class, [
'fqn' => $this->fqn,
'view' => $this->view,
]));
}
protected function createChartWidget(): void
{
if ($this->type !== ChartWidget::class) {
return;
}
$type = select(
label: 'Which type of chart would you like to create?',
options: [
'bar' => 'Bar chart',
'bubble' => 'Bubble chart',
'doughnut' => 'Doughnut chart',
'line' => 'Line chart',
'pie' => 'Pie chart',
'polarArea' => 'Polar area chart',
'radar' => 'Radar chart',
'scatter' => 'Scatter chart',
],
default: 'line',
);
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(ChartWidgetClassGenerator::class, [
'fqn' => $this->fqn,
'type' => $type,
]));
}
protected function createStatsOverviewWidget(): void
{
if ($this->type !== StatsOverviewWidget::class) {
return;
}
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(StatsOverviewWidgetClassGenerator::class, [
'fqn' => $this->fqn,
]));
}
protected function createTableWidget(): void
{
if ($this->type !== TableWidget::class) {
return;
}
$modelFqns = discover_app_classes(parentClass: Model::class);
$modelFqn = suggest(
label: 'What is the model?',
options: function (string $search) use ($modelFqns): array {
$search = str($search)->trim()->replace(['\\', '/'], '');
if (blank($search)) {
return $modelFqns;
}
return array_filter(
$modelFqns,
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
);
},
placeholder: app()->getNamespace() . 'Models\\BlogPost',
);
$isGenerated = confirm(
label: 'Should the table columns be generated from the current database columns?',
default: false,
);
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(TableWidgetClassGenerator::class, [
'fqn' => $this->fqn,
'modelFqn' => $modelFqn ?: Model::class,
'isGenerated' => $isGenerated,
]));
}
protected function createView(): void
{
if (blank($this->view)) {
return;
}
if (! $this->option('force') && $this->checkForCollision($this->viewPath)) {
throw new FailureCommandOutput;
}
$this->copyStubToApp('WidgetView', $this->viewPath);
}
}
@@ -0,0 +1,13 @@
<?php
namespace Filament\Widgets\Concerns;
trait CanPoll
{
protected ?string $pollingInterval = '5s';
protected function getPollingInterval(): ?string
{
return $this->pollingInterval;
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class DoughnutChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'doughnut';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class LineChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'line';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class PieChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'pie';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class PolarAreaChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'polarArea';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class RadarChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'radar';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class ScatterChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'scatter';
}
}
@@ -0,0 +1,114 @@
<?php
namespace Filament\Widgets;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Schemas\Schema;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverviewWidget extends Widget implements HasSchemas
{
use Concerns\CanPoll;
use InteractsWithSchemas;
/**
* @var array<Stat> | null
*/
protected ?array $cachedStats = null;
protected int | string | array $columnSpan = 'full';
protected ?string $heading = null;
protected ?string $description = null;
/**
* @var int | array<string, ?int> | null
*/
protected int | array | null $columns = null;
/**
* @var view-string
*/
protected string $view = 'filament-widgets::stats-overview-widget';
public function content(Schema $schema): Schema
{
return $schema
->components([
$this->getSectionContentComponent(),
]);
}
public function getSectionContentComponent(): Component
{
return Section::make()
->heading($this->getHeading())
->description($this->getDescription())
->schema($this->getCachedStats())
->columns($this->getColumns())
->contained(false)
->gridContainer();
}
/**
* @return int | array<string, ?int> | null
*/
protected function getColumns(): int | array | null
{
if ($this->columns) {
return $this->columns;
}
$count = count($this->getCachedStats());
if ($count < 3) {
return ['@xl' => 3, '!@lg' => 3];
}
if (($count % 3) !== 1) {
return ['@xl' => 3, '!@lg' => 3];
}
return ['@xl' => 4, '!@lg' => 4];
}
protected function getDescription(): ?string
{
return $this->description;
}
protected function getHeading(): ?string
{
return $this->heading;
}
/**
* @return array<Stat>
*/
protected function getCachedStats(): array
{
return $this->cachedStats ??= $this->getStats();
}
/**
* @deprecated Use `getStats()` instead.
*
* @return array<Stat>
*/
protected function getCards(): array
{
return [];
}
/**
* @return array<Stat>
*/
protected function getStats(): array
{
return $this->getCards();
}
}
@@ -0,0 +1,182 @@
<?php
namespace Filament\Widgets\StatsOverviewWidget;
use BackedEnum;
use Closure;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Concerns\CanOpenUrl;
use Filament\Schemas\Components\Concerns\HasDescription;
use Filament\Schemas\Components\Concerns\HasLabel;
use Filament\Support\Concerns\HasColor;
use Filament\Support\Enums\IconPosition;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
class Stat extends Component
{
use CanOpenUrl;
use HasColor;
use HasDescription;
use HasLabel;
protected string $view = 'filament-widgets::stats-overview-widget.stat';
/**
* @var array<float> | null
*/
protected ?array $chart = null;
/**
* @var string | array<string> | null
*/
protected string | array | null $chartColor = null;
protected string | BackedEnum | null $icon = null;
protected string | BackedEnum | null $descriptionIcon = null;
protected IconPosition | string | null $descriptionIconPosition = null;
/**
* @var string | array<string> | null
*/
protected string | array | null $descriptionColor = null;
/**
* @var scalar | Htmlable | Closure
*/
protected $value;
/**
* @param scalar | Htmlable | Closure $value
*/
final public function __construct(string | Htmlable $label, $value)
{
$this->label($label);
$this->value($value);
}
/**
* @param scalar | Htmlable | Closure $value
*/
public static function make(string | Htmlable $label, $value): static
{
return app(static::class, ['label' => $label, 'value' => $value]);
}
/**
* @param string | array<string> | null $color
*/
public function chartColor(string | array | null $color): static
{
$this->chartColor = $color;
return $this;
}
public function icon(string | BackedEnum | null $icon): static
{
$this->icon = $icon;
return $this;
}
/**
* @param string | array<string> | null $color
*/
public function descriptionColor(string | array | null $color): static
{
$this->descriptionColor = $color;
return $this;
}
public function descriptionIcon(string | BackedEnum | null $icon, IconPosition | string | null $position = null): static
{
$this->descriptionIcon = $icon;
$this->descriptionIconPosition = $position;
return $this;
}
/**
* @param array<float> | Arrayable | null $chart
*/
public function chart(array | Arrayable | null $chart): static
{
if (is_null($chart)) {
return $this;
}
if ($chart instanceof Arrayable) {
$chart = $chart->toArray();
}
$this->chart = $chart;
return $this;
}
/**
* @param scalar | Htmlable | Closure $value
*/
public function value($value): static
{
$this->value = $value;
return $this;
}
/**
* @return array<float> | null
*/
public function getChart(): ?array
{
return $this->chart;
}
/**
* @return string | array<string> | null
*/
public function getChartColor(): string | array | null
{
return $this->chartColor ?? $this->getColor();
}
public function getIcon(): string | BackedEnum | Htmlable | null
{
return $this->icon;
}
/**
* @return string | array<string> | null
*/
public function getDescriptionColor(): string | array | null
{
return $this->descriptionColor ?? $this->getColor();
}
public function getDescriptionIcon(): string | BackedEnum | Htmlable | null
{
return $this->descriptionIcon;
}
public function getDescriptionIconPosition(): IconPosition | string
{
return $this->descriptionIconPosition ?? IconPosition::After;
}
/**
* @return scalar | Htmlable | Closure
*/
public function getValue(): mixed
{
return value($this->value);
}
public function generateChartDataChecksum(): string
{
return md5(json_encode($this->getChart()) . now());
}
}
@@ -0,0 +1,53 @@
<?php
namespace Filament\Widgets;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Enums\PaginationMode;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
class TableWidget extends Widget implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use InteractsWithTable {
makeTable as makeBaseTable;
}
/**
* @var view-string
*/
protected string $view = 'filament-widgets::table-widget';
/**
* @deprecated Override the `table()` method to configure the table.
*/
protected static ?string $heading = null;
/**
* @deprecated Override the `table()` method to configure the table.
*/
protected function getTableHeading(): string | Htmlable | null
{
return static::$heading;
}
protected function makeTable(): Table
{
return $this->makeBaseTable()
->heading(
$this->getTableHeading() ?? (string) str(class_basename(static::class))
->beforeLast('Widget')
->kebab()
->replace('-', ' ')
->ucwords(),
)
->paginationMode(PaginationMode::Simple);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Widgets\View\Components;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class ChartWidgetComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}
@@ -0,0 +1,57 @@
<?php
namespace Filament\Widgets\View\Components\StatsOverviewWidgetComponent\StatComponent;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class DescriptionComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
$gray = FilamentColor::getColor('gray');
ksort($color);
foreach (array_keys($color) as $shade) {
if ($shade < 600) {
continue;
}
if (Color::isTextContrastRatioAccessible('oklch(1 0 0)', $color[$shade])) {
$text = $shade;
break;
}
}
$text ??= 950;
krsort($color);
foreach (array_keys($color) as $shade) {
if ($shade > 400) {
continue;
}
if (Color::isTextContrastRatioAccessible($gray[900], $color[$shade])) {
$darkText = $shade;
break;
}
}
$darkText ??= 200;
return [
'text' => $text,
'dark:text' => $darkText,
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Widgets\View\Components\StatsOverviewWidgetComponent\StatComponent;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class StatsOverviewWidgetStatChartComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}
@@ -0,0 +1,8 @@
<?php
namespace Filament\Widgets\View;
class WidgetsIconAlias
{
const CHART_WIDGET_FILTER = 'widgets::chart-widget.filter';
}
@@ -0,0 +1,10 @@
<?php
namespace Filament\Widgets\View;
class WidgetsRenderHook
{
const TABLE_WIDGET_END = 'widgets::table-widget.end';
const TABLE_WIDGET_START = 'widgets::table-widget.start';
}
@@ -0,0 +1,108 @@
<?php
namespace Filament\Widgets;
use Filament\Support\Concerns\CanBeLazy;
use Illuminate\Contracts\View\View;
use Livewire\Component;
abstract class Widget extends Component
{
use CanBeLazy;
protected static bool $isDiscovered = true;
protected static ?int $sort = null;
/**
* @var view-string
*/
protected string $view;
/**
* @var int | string | array<string, int | null>
*/
protected int | string | array $columnSpan = 1;
/**
* @var int | string | array<string, int | null>
*/
protected int | string | array $columnStart = [];
public static function canView(): bool
{
return true;
}
public static function getSort(): int
{
return static::$sort ?? -1;
}
/**
* @return int | string | array<string, int | null>
*/
public function getColumnSpan(): int | string | array
{
return $this->columnSpan;
}
/**
* @return int | string | array<string, int | null>
*/
public function getColumnStart(): int | string | array
{
return $this->columnStart;
}
/**
* @return array<string, mixed>
*/
protected function getViewData(): array
{
return [];
}
public static function isDiscovered(): bool
{
return static::$isDiscovered;
}
public function render(): View
{
return view($this->view, $this->getViewData());
}
/**
* @param array<string, mixed> $properties
*/
public static function make(array $properties = []): WidgetConfiguration
{
return app(WidgetConfiguration::class, ['widget' => static::class, 'properties' => $properties]);
}
/**
* @return array<string, mixed>
*/
public function getPlaceholderData(): array
{
return [
'columnSpan' => $this->getColumnSpan(),
'columnStart' => $this->getColumnStart(),
];
}
/**
* @return array<string, mixed>
*/
public static function getDefaultProperties(): array
{
$properties = [];
if (static::isLazy()) {
$properties['lazy'] = true;
}
return $properties;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Filament\Widgets;
class WidgetConfiguration
{
/**
* @param class-string<Widget> $widget
* @param array<string, mixed> $properties
*/
public function __construct(
public readonly string $widget,
protected array $properties = [],
) {}
/**
* @return array<string, mixed>
*/
public function getProperties(): array
{
return $this->properties;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Filament\Widgets;
use Filament\Support\Assets\AlpineComponent;
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Filesystem\Filesystem;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
class WidgetsServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('filament-widgets')
->hasCommands([
Commands\MakeWidgetCommand::class,
])
->hasTranslations()
->hasViews();
}
public function packageBooted(): void
{
FilamentAsset::register([
AlpineComponent::make('chart', __DIR__ . '/../dist/components/chart.js'),
AlpineComponent::make('stats-overview/stat/chart', __DIR__ . '/../dist/components/stats-overview/stat/chart.js'),
], 'filament/widgets');
if ($this->app->runningInConsole()) {
foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) {
$this->publishes([
$file->getRealPath() => base_path("stubs/filament/{$file->getFilename()}"),
], 'filament-stubs');
}
}
}
}