diff --git a/packages/panels/src/Commands/FileGenerators/CustomPageClassGenerator.php b/packages/panels/src/Commands/FileGenerators/CustomPageClassGenerator.php
index 3958265a16..5e230225f7 100644
--- a/packages/panels/src/Commands/FileGenerators/CustomPageClassGenerator.php
+++ b/packages/panels/src/Commands/FileGenerators/CustomPageClassGenerator.php
@@ -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;
diff --git a/packages/panels/src/Commands/FileGenerators/Resources/Pages/ResourceCustomPageClassGenerator.php b/packages/panels/src/Commands/FileGenerators/Resources/Pages/ResourceCustomPageClassGenerator.php
index 7d6ec40c9d..a9e52a10b0 100644
--- a/packages/panels/src/Commands/FileGenerators/Resources/Pages/ResourceCustomPageClassGenerator.php
+++ b/packages/panels/src/Commands/FileGenerators/Resources/Pages/ResourceCustomPageClassGenerator.php
@@ -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;
diff --git a/packages/panels/src/Commands/MakeClusterCommand.php b/packages/panels/src/Commands/MakeClusterCommand.php
index 117c0aa2cd..8ab98f5581 100644
--- a/packages/panels/src/Commands/MakeClusterCommand.php
+++ b/packages/panels/src/Commands/MakeClusterCommand.php
@@ -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;
diff --git a/packages/panels/src/Commands/MakePageCommand.php b/packages/panels/src/Commands/MakePageCommand.php
index 63f7dd8383..851a86e7a7 100644
--- a/packages/panels/src/Commands/MakePageCommand.php
+++ b/packages/panels/src/Commands/MakePageCommand.php
@@ -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',
diff --git a/packages/panels/src/Commands/MakeRelationManagerCommand.php b/packages/panels/src/Commands/MakeRelationManagerCommand.php
index 10e1986d89..acf059a574 100644
--- a/packages/panels/src/Commands/MakeRelationManagerCommand.php
+++ b/packages/panels/src/Commands/MakeRelationManagerCommand.php
@@ -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;
diff --git a/packages/panels/src/Commands/MakeResourceCommand.php b/packages/panels/src/Commands/MakeResourceCommand.php
index ea3244e159..7e7306cb0a 100644
--- a/packages/panels/src/Commands/MakeResourceCommand.php
+++ b/packages/panels/src/Commands/MakeResourceCommand.php
@@ -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,
diff --git a/packages/panels/src/Commands/MakeThemeCommand.php b/packages/panels/src/Commands/MakeThemeCommand.php
index fd043d701d..1f9f619fb9 100644
--- a/packages/panels/src/Commands/MakeThemeCommand.php
+++ b/packages/panels/src/Commands/MakeThemeCommand.php
@@ -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;
diff --git a/packages/panels/stubs/PageView.stub b/packages/panels/stubs/PageView.stub
index cc61477951..bb8ed159a9 100644
--- a/packages/panels/stubs/PageView.stub
+++ b/packages/panels/stubs/PageView.stub
@@ -1,3 +1,3 @@
-
+ {{-- Page content --}}
diff --git a/packages/spatie-laravel-settings-plugin/src/Commands/MakeSettingsPageCommand.php b/packages/spatie-laravel-settings-plugin/src/Commands/MakeSettingsPageCommand.php
index 6794dc62e6..5c044da9d8 100644
--- a/packages/spatie-laravel-settings-plugin/src/Commands/MakeSettingsPageCommand.php
+++ b/packages/spatie-laravel-settings-plugin/src/Commands/MakeSettingsPageCommand.php
@@ -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(
diff --git a/packages/panels/src/Commands/Concerns/CanAskForRelatedModel.php b/packages/support/src/Commands/Concerns/CanAskForRelatedModel.php
similarity index 89%
rename from packages/panels/src/Commands/Concerns/CanAskForRelatedModel.php
rename to packages/support/src/Commands/Concerns/CanAskForRelatedModel.php
index f2baeb53a4..efb5768adc 100644
--- a/packages/panels/src/Commands/Concerns/CanAskForRelatedModel.php
+++ b/packages/support/src/Commands/Concerns/CanAskForRelatedModel.php
@@ -1,6 +1,6 @@
is_subclass_of($modelFqn, Model::class),
+ fn (string $class): bool => is_subclass_of($class, Model::class),
);
return search(
diff --git a/packages/panels/src/Commands/Concerns/CanAskForRelatedResource.php b/packages/support/src/Commands/Concerns/CanAskForRelatedResource.php
similarity index 96%
rename from packages/panels/src/Commands/Concerns/CanAskForRelatedResource.php
rename to packages/support/src/Commands/Concerns/CanAskForRelatedResource.php
index 4043a79fc5..df6b4508c6 100644
--- a/packages/panels/src/Commands/Concerns/CanAskForRelatedResource.php
+++ b/packages/support/src/Commands/Concerns/CanAskForRelatedResource.php
@@ -1,6 +1,6 @@
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 */
diff --git a/packages/panels/src/Commands/Concerns/HasResourcesLocation.php b/packages/support/src/Commands/Concerns/HasResourcesLocation.php
similarity index 97%
rename from packages/panels/src/Commands/Concerns/HasResourcesLocation.php
rename to packages/support/src/Commands/Concerns/HasResourcesLocation.php
index e5894400b7..c06edc2bce 100644
--- a/packages/panels/src/Commands/Concerns/HasResourcesLocation.php
+++ b/packages/support/src/Commands/Concerns/HasResourcesLocation.php
@@ -1,6 +1,6 @@
classBasename()
->kebab()
->replace('-', ' ');
}
diff --git a/packages/tables/src/Commands/MakeLivewireTableCommand.php b/packages/tables/src/Commands/MakeLivewireTableCommand.php
index 878f1d428a..a2c94dc958 100644
--- a/packages/tables/src/Commands/MakeLivewireTableCommand.php
+++ b/packages/tables/src/Commands/MakeLivewireTableCommand.php
@@ -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
diff --git a/packages/tables/src/Commands/MakeTableCommand.php b/packages/tables/src/Commands/MakeTableCommand.php
index 6385c656a2..2a01537438 100644
--- a/packages/tables/src/Commands/MakeTableCommand.php
+++ b/packages/tables/src/Commands/MakeTableCommand.php
@@ -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
diff --git a/packages/widgets/src/Commands/FileGenerators/ChartWidgetClassGenerator.php b/packages/widgets/src/Commands/FileGenerators/ChartWidgetClassGenerator.php
new file mode 100644
index 0000000000..53c7b393f2
--- /dev/null
+++ b/packages/widgets/src/Commands/FileGenerators/ChartWidgetClassGenerator.php
@@ -0,0 +1,115 @@
+extractNamespace($this->getFqn());
+ }
+
+ /**
+ * @return array
+ */
+ 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;
+ }
+}
diff --git a/packages/widgets/src/Commands/FileGenerators/CustomWidgetClassGenerator.php b/packages/widgets/src/Commands/FileGenerators/CustomWidgetClassGenerator.php
new file mode 100644
index 0000000000..0c5ee90517
--- /dev/null
+++ b/packages/widgets/src/Commands/FileGenerators/CustomWidgetClassGenerator.php
@@ -0,0 +1,61 @@
+extractNamespace($this->getFqn());
+ }
+
+ /**
+ * @return array
+ */
+ 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;
+ }
+}
diff --git a/packages/widgets/src/Commands/FileGenerators/StatsOverviewWidgetClassGenerator.php b/packages/widgets/src/Commands/FileGenerators/StatsOverviewWidgetClassGenerator.php
new file mode 100644
index 0000000000..e8cae0d245
--- /dev/null
+++ b/packages/widgets/src/Commands/FileGenerators/StatsOverviewWidgetClassGenerator.php
@@ -0,0 +1,71 @@
+extractNamespace($this->getFqn());
+ }
+
+ /**
+ * @return array
+ */
+ 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;
+ }
+}
diff --git a/packages/widgets/src/Commands/FileGenerators/TableWidgetClassGenerator.php b/packages/widgets/src/Commands/FileGenerators/TableWidgetClassGenerator.php
new file mode 100644
index 0000000000..4bf3d66e5c
--- /dev/null
+++ b/packages/widgets/src/Commands/FileGenerators/TableWidgetClassGenerator.php
@@ -0,0 +1,97 @@
+ $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
+ */
+ 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
+ */
+ public function getModelFqn(): ?string
+ {
+ return $this->modelFqn;
+ }
+
+ public function isGenerated(): bool
+ {
+ return $this->isGenerated;
+ }
+}
diff --git a/packages/widgets/src/Commands/MakeWidgetCommand.php b/packages/widgets/src/Commands/MakeWidgetCommand.php
index d94fa768a3..7b87448523 100644
--- a/packages/widgets/src/Commands/MakeWidgetCommand.php
+++ b/packages/widgets/src/Commands/MakeWidgetCommand.php
@@ -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
@@ -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 | null
+ */
+ protected ?string $type = null;
+
+ protected string $widgetsNamespace;
+
+ protected string $widgetsDirectory;
+
+ /**
+ * @return array
+ */
+ 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
+ */
+ 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);
}
}
diff --git a/packages/widgets/stubs/ChartWidget.stub b/packages/widgets/stubs/ChartWidget.stub
deleted file mode 100644
index 65332d835a..0000000000
--- a/packages/widgets/stubs/ChartWidget.stub
+++ /dev/null
@@ -1,22 +0,0 @@
-query(
- // ...
- )
- ->columns([
- // ...
- ]);
- }
-}
diff --git a/packages/widgets/stubs/Widget.stub b/packages/widgets/stubs/Widget.stub
deleted file mode 100644
index f87af96763..0000000000
--- a/packages/widgets/stubs/Widget.stub
+++ /dev/null
@@ -1,10 +0,0 @@
-