mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Make widget command
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
namespace Filament\Commands\FileGenerators;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
use Filament\Commands\FileGenerators\Concerns\CanGenerateViewProperty;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Commands\FileGenerators\ClassGenerator;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanGenerateViewProperty;
|
||||
use Nette\PhpGenerator\ClassType;
|
||||
use Nette\PhpGenerator\Literal;
|
||||
use Nette\PhpGenerator\Property;
|
||||
|
||||
+1
-1
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Filament\Commands\FileGenerators\Resources\Pages;
|
||||
|
||||
use Filament\Commands\FileGenerators\Concerns\CanGenerateViewProperty;
|
||||
use Filament\Commands\FileGenerators\Resources\Pages\Concerns\CanGenerateResourceProperty;
|
||||
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Support\Commands\FileGenerators\ClassGenerator;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanGenerateViewProperty;
|
||||
use Nette\PhpGenerator\ClassType;
|
||||
use Nette\PhpGenerator\Method;
|
||||
use Nette\PhpGenerator\TraitUse;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace Filament\Commands;
|
||||
|
||||
use Filament\Commands\Concerns\HasPanel;
|
||||
use Filament\Commands\FileGenerators\ClusterClassGenerator;
|
||||
use Filament\Support\Commands\Concerns\CanManipulateFiles;
|
||||
use Filament\Support\Commands\Concerns\HasPanel;
|
||||
use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
|
||||
use Filament\Support\Commands\FileGenerators\FileGenerationFlag;
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
namespace Filament\Commands;
|
||||
|
||||
use Filament\Commands\Concerns\CanAskForRelatedModel;
|
||||
use Filament\Commands\Concerns\CanAskForRelatedResource;
|
||||
use Filament\Commands\Concerns\CanAskForResource;
|
||||
use Filament\Commands\Concerns\CanAskForSchema;
|
||||
use Filament\Commands\Concerns\HasCluster;
|
||||
use Filament\Commands\Concerns\HasClusterPagesLocation;
|
||||
use Filament\Commands\Concerns\HasPanel;
|
||||
use Filament\Commands\Concerns\HasResourcesLocation;
|
||||
use Filament\Commands\FileGenerators\CustomPageClassGenerator;
|
||||
use Filament\Commands\FileGenerators\Resources\Pages\ResourceCreateRecordPageClassGenerator;
|
||||
use Filament\Commands\FileGenerators\Resources\Pages\ResourceCustomPageClassGenerator;
|
||||
@@ -21,8 +13,16 @@ use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Resources\Pages\ManageRelatedRecords;
|
||||
use Filament\Resources\Pages\Page as ResourcePage;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Support\Commands\Concerns\CanAskForRelatedModel;
|
||||
use Filament\Support\Commands\Concerns\CanAskForRelatedResource;
|
||||
use Filament\Support\Commands\Concerns\CanAskForResource;
|
||||
use Filament\Support\Commands\Concerns\CanAskForSchema;
|
||||
use Filament\Support\Commands\Concerns\CanAskForViewLocation;
|
||||
use Filament\Support\Commands\Concerns\CanManipulateFiles;
|
||||
use Filament\Support\Commands\Concerns\HasCluster;
|
||||
use Filament\Support\Commands\Concerns\HasClusterPagesLocation;
|
||||
use Filament\Support\Commands\Concerns\HasPanel;
|
||||
use Filament\Support\Commands\Concerns\HasResourcesLocation;
|
||||
use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
|
||||
use Filament\Support\Commands\FileGenerators\FileGenerationFlag;
|
||||
@@ -136,7 +136,7 @@ class MakePageCommand extends Command
|
||||
name: 'panel',
|
||||
shortcut: null,
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'The panel to create the resource in',
|
||||
description: 'The panel to create the page in',
|
||||
),
|
||||
new InputOption(
|
||||
name: 'resource',
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace Filament\Commands;
|
||||
|
||||
use Filament\Commands\Concerns\CanAskForRelatedModel;
|
||||
use Filament\Commands\Concerns\CanAskForRelatedResource;
|
||||
use Filament\Commands\Concerns\CanAskForResource;
|
||||
use Filament\Commands\Concerns\CanAskForSchema;
|
||||
use Filament\Commands\Concerns\HasCluster;
|
||||
use Filament\Commands\Concerns\HasPanel;
|
||||
use Filament\Commands\Concerns\HasResourcesLocation;
|
||||
use Filament\Commands\FileGenerators\Resources\RelationManagerClassGenerator;
|
||||
use Filament\Support\Commands\Concerns\CanAskForRelatedModel;
|
||||
use Filament\Support\Commands\Concerns\CanAskForRelatedResource;
|
||||
use Filament\Support\Commands\Concerns\CanAskForResource;
|
||||
use Filament\Support\Commands\Concerns\CanAskForSchema;
|
||||
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\InvalidCommandOutput;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
|
||||
use Filament\Support\Commands\FileGenerators\FileGenerationFlag;
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
namespace Filament\Commands;
|
||||
|
||||
use Filament\Commands\Concerns\CanAskForResource;
|
||||
use Filament\Commands\Concerns\HasCluster;
|
||||
use Filament\Commands\Concerns\HasPanel;
|
||||
use Filament\Commands\Concerns\HasResourcesLocation;
|
||||
use Filament\Commands\FileGenerators\Resources\Pages\ResourceCreateRecordPageClassGenerator;
|
||||
use Filament\Commands\FileGenerators\Resources\Pages\ResourceEditRecordPageClassGenerator;
|
||||
use Filament\Commands\FileGenerators\Resources\Pages\ResourceListRecordsPageClassGenerator;
|
||||
@@ -16,7 +12,11 @@ use Filament\Commands\FileGenerators\Resources\Schemas\ResourceFormSchemaClassGe
|
||||
use Filament\Commands\FileGenerators\Resources\Schemas\ResourceInfolistSchemaClassGenerator;
|
||||
use Filament\Commands\FileGenerators\Resources\Schemas\ResourceTableClassGenerator;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Support\Commands\Concerns\CanAskForResource;
|
||||
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\InvalidCommandOutput;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
|
||||
use Filament\Support\Commands\FileGenerators\FileGenerationFlag;
|
||||
@@ -267,47 +267,52 @@ class MakeResourceCommand extends Command
|
||||
|
||||
protected function configureModel(): void
|
||||
{
|
||||
$modelNamespace = $this->option('model-namespace') ?? 'App\\Models';
|
||||
if ($this->argument('model')) {
|
||||
$this->modelFqnEnd = (string) str($this->argument('model'))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->when(
|
||||
fn (Stringable $model): bool => str($model)->endsWith('Resource'),
|
||||
fn (Stringable $model): Stringable => str($model)->beforeLast('Resource'),
|
||||
)
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
|
||||
$modelFqns = collect(get_declared_classes())
|
||||
->filter(fn (string $class): bool => is_subclass_of($class, Model::class) &&
|
||||
str($class)->startsWith("{$modelNamespace}\\"))
|
||||
->map(fn (string $class): string => str($class)->after("{$modelNamespace}\\"))
|
||||
->all();
|
||||
if (blank($this->modelFqnEnd)) {
|
||||
$this->modelFqnEnd = 'Resource';
|
||||
}
|
||||
|
||||
$this->modelFqnEnd = (string) str($this->argument('model') ?? suggest(
|
||||
label: 'What is the model name?',
|
||||
options: function (string $search) use ($modelFqns): array {
|
||||
$search = str($search)->trim()->replace(['\\', '/'], '');
|
||||
$modelNamespace = $this->option('model-namespace') ?? 'App\\Models';
|
||||
|
||||
if (blank($search)) {
|
||||
return $modelFqns;
|
||||
}
|
||||
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
|
||||
} else {
|
||||
$modelFqns = collect(get_declared_classes())
|
||||
->filter(fn (string $class): bool => is_subclass_of($class, Model::class) &&
|
||||
(! str((new ReflectionClass($class))->getFileName())->startsWith(base_path('vendor'))))
|
||||
->all();
|
||||
|
||||
return array_filter(
|
||||
$modelFqns,
|
||||
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
|
||||
);
|
||||
},
|
||||
placeholder: 'BlogPost',
|
||||
required: true,
|
||||
))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->when(
|
||||
fn (Stringable $model): bool => str($model)->endsWith('Resource'),
|
||||
fn (Stringable $model): Stringable => str($model)->beforeLast('Resource'),
|
||||
)
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
$this->modelFqn = suggest(
|
||||
label: 'What is the model?',
|
||||
options: function (string $search) use ($modelFqns): array {
|
||||
$search = str($search)->trim()->replace(['\\', '/'], '');
|
||||
|
||||
if (blank($this->modelFqnEnd)) {
|
||||
$this->modelFqnEnd = 'Resource';
|
||||
if (blank($search)) {
|
||||
return $modelFqns;
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
$modelFqns,
|
||||
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
|
||||
);
|
||||
},
|
||||
placeholder: 'App\\Models\\BlogPost',
|
||||
required: true,
|
||||
);
|
||||
|
||||
$this->modelFqnEnd = class_basename($this->modelFqn);
|
||||
}
|
||||
|
||||
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
|
||||
|
||||
if ($this->option('model')) {
|
||||
$this->callSilently('make:model', [
|
||||
'name' => $this->modelFqn,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Filament\Commands;
|
||||
|
||||
use Filament\Commands\Concerns\HasPanel;
|
||||
use Filament\Support\Commands\Concerns\CanManipulateFiles;
|
||||
use Filament\Support\Commands\Concerns\HasPanel;
|
||||
use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Filament\Support\Commands\Exceptions\SuccessCommandOutput;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
|
||||
{{-- Page content --}}
|
||||
</x-filament-panels::page>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Filament\Commands;
|
||||
|
||||
use Filament\Commands\Concerns\HasCluster;
|
||||
use Filament\Commands\Concerns\HasClusterPagesLocation;
|
||||
use Filament\Commands\Concerns\HasPanel;
|
||||
use Filament\Commands\FileGenerators\SettingsPageClassGenerator;
|
||||
use Filament\Support\Commands\Concerns\CanManipulateFiles;
|
||||
use Filament\Support\Commands\Concerns\HasCluster;
|
||||
use Filament\Support\Commands\Concerns\HasClusterPagesLocation;
|
||||
use Filament\Support\Commands\Concerns\HasPanel;
|
||||
use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
@@ -170,7 +170,7 @@ class MakeSettingsPageCommand extends Command
|
||||
|
||||
$settingsFqns = array_filter(
|
||||
get_declared_classes(),
|
||||
fn (string $modelFqn): bool => is_subclass_of($modelFqn, Settings::class),
|
||||
fn (string $class): bool => is_subclass_of($class, Settings::class),
|
||||
);
|
||||
|
||||
$this->settingsFqn = search(
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -15,7 +15,7 @@ trait CanAskForRelatedModel
|
||||
{
|
||||
$modelFqns = array_filter(
|
||||
get_declared_classes(),
|
||||
fn (string $modelFqn): bool => is_subclass_of($modelFqn, Model::class),
|
||||
fn (string $class): bool => is_subclass_of($class, Model::class),
|
||||
);
|
||||
|
||||
return search(
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Support\Collection;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\suggest;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
use ReflectionClass;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use ReflectionClass;
|
||||
|
||||
+15
-2
@@ -1,19 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
trait HasPanel
|
||||
{
|
||||
protected ?Panel $panel;
|
||||
|
||||
protected function configurePanel(string $question): void
|
||||
protected function configurePanel(string $question, ?string $initialQuestion = null): void
|
||||
{
|
||||
if (! class_exists(Panel::class)) {
|
||||
$this->panel = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$panelName = $this->option('panel');
|
||||
|
||||
$this->panel = filled($panelName) ? Filament::getPanel($panelName, isStrict: false) : null;
|
||||
@@ -22,6 +29,12 @@ trait HasPanel
|
||||
return;
|
||||
}
|
||||
|
||||
if (filled($initialQuestion) && (! confirm(label: $initialQuestion))) {
|
||||
$this->panel = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$panels = Filament::getPanels();
|
||||
|
||||
/** @var Panel $panel */
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\Concerns;
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Commands\FileGenerators\Concerns;
|
||||
namespace Filament\Support\Commands\FileGenerators\Concerns;
|
||||
|
||||
use Nette\PhpGenerator\ClassType;
|
||||
use Nette\PhpGenerator\Property;
|
||||
@@ -47,7 +47,8 @@ if (! function_exists('Filament\Support\get_model_label')) {
|
||||
*/
|
||||
function get_model_label(string $model): string
|
||||
{
|
||||
return (string) str(class_basename($model))
|
||||
return (string) str($model)
|
||||
->classBasename()
|
||||
->kebab()
|
||||
->replace('-', ' ');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Filament\Tables\Commands\FileGenerators\LivewireTableComponentClassGenerator
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -144,16 +145,28 @@ class MakeLivewireTableCommand extends Command
|
||||
|
||||
protected function configureModel(): void
|
||||
{
|
||||
$modelNamespace = $this->option('model-namespace') ?? 'App\\Models';
|
||||
if ($this->argument('model')) {
|
||||
$this->modelFqnEnd = (string) str($this->argument('model'))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
|
||||
$modelNamespace = $this->option('model-namespace') ?? 'App\\Models';
|
||||
|
||||
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$modelFqns = collect(get_declared_classes())
|
||||
->filter(fn (string $class): bool => is_subclass_of($class, Model::class) &&
|
||||
str($class)->startsWith("{$modelNamespace}\\"))
|
||||
->map(fn (string $class): string => str($class)->after("{$modelNamespace}\\"))
|
||||
(! str((new ReflectionClass($class))->getFileName())->startsWith(base_path('vendor'))))
|
||||
->all();
|
||||
|
||||
$this->modelFqnEnd = (string) str($this->argument('model') ?? suggest(
|
||||
label: 'What is the model name?',
|
||||
$this->modelFqn = suggest(
|
||||
label: 'What is the model?',
|
||||
options: function (string $search) use ($modelFqns): array {
|
||||
$search = str($search)->trim()->replace(['\\', '/'], '');
|
||||
|
||||
@@ -166,15 +179,11 @@ class MakeLivewireTableCommand extends Command
|
||||
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
|
||||
);
|
||||
},
|
||||
placeholder: 'BlogPost',
|
||||
))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
placeholder: 'App\\Models\\BlogPost',
|
||||
required: true,
|
||||
);
|
||||
|
||||
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
|
||||
$this->modelFqnEnd = class_basename($this->modelFqn);
|
||||
}
|
||||
|
||||
protected function configureIsGenerated(): void
|
||||
|
||||
@@ -8,6 +8,7 @@ use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Filament\Tables\Commands\FileGenerators\TableClassGenerator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -134,16 +135,28 @@ class MakeTableCommand extends Command
|
||||
|
||||
protected function configureModel(): void
|
||||
{
|
||||
$modelNamespace = $this->option('model-namespace') ?? 'App\\Models';
|
||||
if ($this->argument('model')) {
|
||||
$this->modelFqnEnd = (string) str($this->argument('model'))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
|
||||
$modelNamespace = $this->option('model-namespace') ?? 'App\\Models';
|
||||
|
||||
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$modelFqns = collect(get_declared_classes())
|
||||
->filter(fn (string $class): bool => is_subclass_of($class, Model::class) &&
|
||||
str($class)->startsWith("{$modelNamespace}\\"))
|
||||
->map(fn (string $class): string => str($class)->after("{$modelNamespace}\\"))
|
||||
(! str((new ReflectionClass($class))->getFileName())->startsWith(base_path('vendor'))))
|
||||
->all();
|
||||
|
||||
$this->modelFqnEnd = (string) str($this->argument('model') ?? suggest(
|
||||
label: 'What is the model name?',
|
||||
$this->modelFqn = suggest(
|
||||
label: 'What is the model?',
|
||||
options: function (string $search) use ($modelFqns): array {
|
||||
$search = str($search)->trim()->replace(['\\', '/'], '');
|
||||
|
||||
@@ -156,15 +169,11 @@ class MakeTableCommand extends Command
|
||||
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
|
||||
);
|
||||
},
|
||||
placeholder: 'BlogPost',
|
||||
))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
placeholder: 'App\\Models\\BlogPost',
|
||||
required: true,
|
||||
);
|
||||
|
||||
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
|
||||
$this->modelFqnEnd = class_basename($this->modelFqn);
|
||||
}
|
||||
|
||||
protected function configureIsGenerated(): void
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?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('-', ' ')
|
||||
->title(),
|
||||
)
|
||||
->setProtected()
|
||||
->setStatic()
|
||||
->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\CanGenerateTables;
|
||||
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 CanGenerateTables;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,38 @@
|
||||
|
||||
namespace Filament\Widgets\Commands;
|
||||
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Resource;
|
||||
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\InvalidCommandOutput;
|
||||
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 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: [
|
||||
@@ -20,11 +42,18 @@ use function Laravel\Prompts\text;
|
||||
])]
|
||||
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 $signature = 'make:filament-widget {name?} {--R|resource=} {--C|chart} {--T|table} {--S|stats-overview} {--panel=} {--F|force}';
|
||||
protected $name = 'make:filament-widget';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
@@ -34,9 +63,137 @@ class MakeWidgetCommand extends Command
|
||||
'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: '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
|
||||
{
|
||||
$widget = (string) str($this->argument('name') ?? text(
|
||||
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 (InvalidCommandOutput) {
|
||||
return static::INVALID;
|
||||
}
|
||||
|
||||
$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,
|
||||
@@ -44,210 +201,315 @@ class MakeWidgetCommand extends Command
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
$widgetClass = (string) str($widget)->classBasename();
|
||||
$widgetNamespace = str($widget)->contains('\\') ?
|
||||
(string) str($widget)->beforeLast('\\') :
|
||||
'';
|
||||
}
|
||||
|
||||
$resource = null;
|
||||
$resourceClass = null;
|
||||
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',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
$type = match (true) {
|
||||
$this->option('chart') => 'Chart',
|
||||
$this->option('stats-overview') => 'Stats overview',
|
||||
$this->option('table') => 'Table',
|
||||
default => select(
|
||||
label: 'What type of widget do you want to create?',
|
||||
options: ['Chart', 'Stats overview', 'Table', 'Custom'],
|
||||
),
|
||||
};
|
||||
protected function configureHasResource(): void
|
||||
{
|
||||
if (! $this->panel) {
|
||||
$this->hasResource = false;
|
||||
|
||||
if (class_exists(Resource::class)) {
|
||||
$resourceInput = $this->option('resource') ?? text(
|
||||
label: 'What is the resource you would like to create this in?',
|
||||
placeholder: '[Optional] BlogPostResource',
|
||||
);
|
||||
|
||||
if (filled($resourceInput)) {
|
||||
$resource = (string) str($resourceInput)
|
||||
->studly()
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->replace('/', '\\');
|
||||
|
||||
if (! str($resource)->endsWith('Resource')) {
|
||||
$resource .= 'Resource';
|
||||
}
|
||||
|
||||
$resourceClass = (string) str($resource)
|
||||
->classBasename();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$panel = null;
|
||||
$this->hasResource = $this->option('resource') || confirm(
|
||||
label: 'Would you like to create this widget in a resource?',
|
||||
default: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (class_exists(Panel::class)) {
|
||||
$panel = $this->option('panel');
|
||||
|
||||
if ($panel) {
|
||||
$panel = Filament::getPanel($panel, isStrict: false);
|
||||
}
|
||||
|
||||
if (! $panel) {
|
||||
$panels = Filament::getPanels();
|
||||
$namespace = config('livewire.class_namespace');
|
||||
|
||||
/** @var ?Panel $panel */
|
||||
$panel = $panels[select(
|
||||
label: 'Where would you like to create this?',
|
||||
options: array_unique([
|
||||
...array_map(
|
||||
fn (Panel $panel): string => "The [{$panel->getId()}] panel",
|
||||
$panels,
|
||||
),
|
||||
$namespace => "[{$namespace}] alongside other Livewire components",
|
||||
])
|
||||
)] ?? null;
|
||||
}
|
||||
protected function configureCluster(): void
|
||||
{
|
||||
if (! $this->hasResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = null;
|
||||
$namespace = null;
|
||||
$resourcePath = null;
|
||||
$resourceNamespace = null;
|
||||
|
||||
if (! $panel) {
|
||||
$namespace = config('livewire.class_namespace');
|
||||
$path = app_path((string) str($namespace)->after('App\\')->replace('\\', '/'));
|
||||
} elseif ($resource === null) {
|
||||
$widgetDirectories = $panel->getWidgetDirectories();
|
||||
$widgetNamespaces = $panel->getWidgetNamespaces();
|
||||
|
||||
foreach ($widgetDirectories as $widgetIndex => $widgetDirectory) {
|
||||
if (str($widgetDirectory)->startsWith(base_path('vendor'))) {
|
||||
unset($widgetDirectories[$widgetIndex]);
|
||||
unset($widgetNamespaces[$widgetIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
$namespace = (count($widgetNamespaces) > 1) ?
|
||||
select(
|
||||
label: 'Which namespace would you like to create this in?',
|
||||
options: $widgetNamespaces,
|
||||
) :
|
||||
(Arr::first($widgetNamespaces) ?? 'App\\Filament\\Widgets');
|
||||
$path = (count($widgetDirectories) > 1) ?
|
||||
$widgetDirectories[array_search($namespace, $widgetNamespaces)] :
|
||||
(Arr::first($widgetDirectories) ?? app_path('Filament/Widgets/'));
|
||||
} else {
|
||||
$resourceDirectories = $panel->getResourceDirectories();
|
||||
$resourceNamespaces = $panel->getResourceNamespaces();
|
||||
|
||||
foreach ($resourceDirectories as $resourceIndex => $resourceDirectory) {
|
||||
if (str($resourceDirectory)->startsWith(base_path('vendor'))) {
|
||||
unset($resourceDirectories[$resourceIndex]);
|
||||
unset($resourceNamespaces[$resourceIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
$resourceNamespace = (count($resourceNamespaces) > 1) ?
|
||||
select(
|
||||
label: 'Which namespace would you like to create this in?',
|
||||
options: $resourceNamespaces,
|
||||
) :
|
||||
(Arr::first($resourceNamespaces) ?? 'App\\Filament\\Resources');
|
||||
$resourcePath = (count($resourceDirectories) > 1) ?
|
||||
$resourceDirectories[array_search($resourceNamespace, $resourceNamespaces)] :
|
||||
(Arr::first($resourceDirectories) ?? app_path('Filament/Resources/'));
|
||||
}
|
||||
|
||||
$view = str($widget)->prepend(
|
||||
(string) str($resource === null ? ($panel ? "{$namespace}\\" : 'livewire\\') : "{$resourceNamespace}\\{$resource}\\widgets\\")
|
||||
->replaceFirst('App\\', '')
|
||||
)
|
||||
->replace('\\', '/')
|
||||
->explode('/')
|
||||
->map(fn ($segment) => Str::lower(Str::kebab($segment)))
|
||||
->implode('.');
|
||||
|
||||
$path = (string) str($widget)
|
||||
->prepend('/')
|
||||
->prepend($resource === null ? $path : "{$resourcePath}\\{$resource}\\Widgets\\")
|
||||
->replace('\\', '/')
|
||||
->replace('//', '/')
|
||||
->append('.php');
|
||||
|
||||
$viewPath = resource_path(
|
||||
(string) str($view)
|
||||
->replace('.', '/')
|
||||
->prepend('views/')
|
||||
->append('.blade.php'),
|
||||
$this->configureClusterFqn(
|
||||
initialQuestion: 'Is the resource in a cluster?',
|
||||
question: 'Which cluster is the resource in?',
|
||||
);
|
||||
|
||||
if (! $this->option('force') && $this->checkForCollision([
|
||||
$path,
|
||||
...($this->option('stats-overview') || $this->option('chart')) ? [] : [$viewPath],
|
||||
])) {
|
||||
return static::INVALID;
|
||||
if (blank($this->clusterFqn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'Chart') {
|
||||
$chartType = select(
|
||||
label: 'Which type of chart would you like to create?',
|
||||
options: [
|
||||
'Bar chart',
|
||||
'Bubble chart',
|
||||
'Doughnut chart',
|
||||
'Line chart',
|
||||
'Pie chart',
|
||||
'Polar area chart',
|
||||
'Radar chart',
|
||||
'Scatter chart',
|
||||
],
|
||||
$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?',
|
||||
);
|
||||
|
||||
$this->copyStubToApp('ChartWidget', $path, [
|
||||
'class' => $widgetClass,
|
||||
'namespace' => filled($resource) ? "{$resourceNamespace}\\{$resource}\\Widgets" . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : '') : $namespace . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : ''),
|
||||
'type' => match ($chartType) {
|
||||
'Bar chart' => 'bar',
|
||||
'Bubble chart' => 'bubble',
|
||||
'Doughnut chart' => 'doughnut',
|
||||
'Pie chart' => 'pie',
|
||||
'Polar area chart' => 'polarArea',
|
||||
'Radar chart' => 'radar',
|
||||
'Scatter chart' => 'scatter',
|
||||
default => 'line',
|
||||
},
|
||||
]);
|
||||
} elseif ($type === 'Stats overview') {
|
||||
$this->copyStubToApp('StatsOverviewWidget', $path, [
|
||||
'class' => $widgetClass,
|
||||
'namespace' => filled($resource) ? "{$resourceNamespace}\\{$resource}\\Widgets" . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : '') : $namespace . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : ''),
|
||||
]);
|
||||
} elseif ($type === 'Table') {
|
||||
$this->copyStubToApp('TableWidget', $path, [
|
||||
'class' => $widgetClass,
|
||||
'namespace' => filled($resource) ? "{$resourceNamespace}\\{$resource}\\Widgets" . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : '') : $namespace . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : ''),
|
||||
]);
|
||||
} else {
|
||||
$this->copyStubToApp('Widget', $path, [
|
||||
'class' => $widgetClass,
|
||||
'namespace' => filled($resource) ? "{$resourceNamespace}\\{$resource}\\Widgets" . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : '') : $namespace . ($widgetNamespace !== '' ? "\\{$widgetNamespace}" : ''),
|
||||
'view' => $view,
|
||||
]);
|
||||
|
||||
$this->copyStubToApp('WidgetView', $viewPath);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->components->info("Filament widget [{$path}] created successfully.");
|
||||
$directories = $this->panel->getWidgetDirectories();
|
||||
$namespaces = $this->panel->getWidgetNamespaces();
|
||||
|
||||
if ($resource !== null) {
|
||||
$this->components->info("Make sure to register the widget in `{$resourceClass}::getWidgets()`, and then again in `getHeaderWidgets()` or `getFooterWidgets()` of any `{$resourceClass}` page.");
|
||||
foreach ($directories as $index => $directory) {
|
||||
if (str($directory)->startsWith(base_path('vendor'))) {
|
||||
unset($directories[$index]);
|
||||
unset($namespaces[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
return static::SUCCESS;
|
||||
if (count($namespaces) < 2) {
|
||||
$this->widgetsNamespace = (Arr::first($namespaces) ?? 'App\\Filament\\Widgets');
|
||||
$this->widgetsDirectory = (Arr::first($directories) ?? app_path('Filament/Widgets/'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->widgetsNamespace = search(
|
||||
label: 'Which namespace would you like to create this widget in?',
|
||||
options: function (?string $search) use ($namespaces): array {
|
||||
if (blank($search)) {
|
||||
return $namespaces;
|
||||
}
|
||||
|
||||
$search = str($search)->trim()->replace(['\\', '/'], '');
|
||||
|
||||
return array_filter($namespaces, 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 InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$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 InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$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 InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$this->writeFile($path, app(StatsOverviewWidgetClassGenerator::class, [
|
||||
'fqn' => $this->fqn,
|
||||
]));
|
||||
}
|
||||
|
||||
protected function createTableWidget(): void
|
||||
{
|
||||
if ($this->type !== TableWidget::class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modelFqns = collect(get_declared_classes())
|
||||
->filter(fn (string $class): bool => is_subclass_of($class, Model::class) &&
|
||||
(! str((new ReflectionClass($class))->getFileName())->startsWith(base_path('vendor'))))
|
||||
->all();
|
||||
|
||||
$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\\Models\\BlogPost',
|
||||
);
|
||||
|
||||
$isGenerated = confirm(
|
||||
label: 'Would you like to generate the table columns based on the attributes of the model?',
|
||||
default: false,
|
||||
);
|
||||
|
||||
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
|
||||
->replace('\\', '/')
|
||||
->replace('//', '/');
|
||||
|
||||
if (! $this->option('force') && $this->checkForCollision($path)) {
|
||||
throw new InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$this->writeFile($path, app(TableWidgetClassGenerator::class, [
|
||||
'fqn' => $this->fqn,
|
||||
'modelFqn' => $modelFqn,
|
||||
'isGenerated' => $isGenerated,
|
||||
]));
|
||||
}
|
||||
|
||||
protected function createView(): void
|
||||
{
|
||||
if (blank($this->view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && $this->checkForCollision($this->viewPath)) {
|
||||
throw new InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$this->copyStubToApp('WidgetView', $this->viewPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Filament\Widgets\ChartWidget;
|
||||
|
||||
class {{ class }} extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Chart';
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return '{{ type }}';
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
class {{ class }} extends BaseWidget
|
||||
{
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
|
||||
class {{ class }} extends BaseWidget
|
||||
{
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
// ...
|
||||
)
|
||||
->columns([
|
||||
// ...
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class {{ class }} extends Widget
|
||||
{
|
||||
protected static string $view = '{{ view }}';
|
||||
}
|
||||
Reference in New Issue
Block a user