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 @@ -