Add module generator UI and discovery service documentation
This commit is contained in:
@@ -75,6 +75,8 @@ A comprehensive Laravel development environment with Docker for local developmen
|
||||
│ ├── laravel-setup.md # Laravel setup guide
|
||||
│ ├── filament-admin.md # Admin panel docs
|
||||
│ ├── modules.md # Modular architecture guide
|
||||
│ ├── module-generator.md # Admin UI module generator
|
||||
│ ├── menu-management.md # Frontend menu system
|
||||
│ ├── audit-trail.md # Audit trail docs
|
||||
│ ├── site-settings.md # Appearance settings
|
||||
│ ├── testing.md # Pest testing guide
|
||||
|
||||
173
docs/module-generator.md
Normal file
173
docs/module-generator.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Module Generator (Admin UI)
|
||||
|
||||
A visual tool for creating module skeletons through the Filament admin panel.
|
||||
|
||||
> **Development Only**: This tool is only available when `APP_ENV=local`.
|
||||
|
||||
## Access
|
||||
|
||||
Navigate to: **Admin Panel → Development → Module Generator**
|
||||
|
||||
URL: `/admin/module-generator`
|
||||
|
||||
## Features
|
||||
|
||||
- **Visual Form**: Create modules without command line
|
||||
- **Auto Git Branch**: Optionally creates `module/{name}` branch and commits files
|
||||
- **Skeleton Only**: Maximum flexibility - creates structure, you add the models
|
||||
- **Instant Feedback**: Shows generation logs and next steps
|
||||
|
||||
## What Gets Created
|
||||
|
||||
```
|
||||
app/Modules/{ModuleName}/
|
||||
├── Config/{module_name}.php # Module configuration
|
||||
├── Database/
|
||||
│ ├── Migrations/ # Empty, add your migrations
|
||||
│ └── Seeders/ # Empty, add your seeders
|
||||
├── Filament/Resources/ # Empty, add Filament resources
|
||||
├── Http/
|
||||
│ ├── Controllers/{ModuleName}Controller.php
|
||||
│ ├── Middleware/ # Empty
|
||||
│ └── Requests/ # Empty
|
||||
├── Models/ # Empty, add your models
|
||||
├── Policies/ # Empty, add policies
|
||||
├── Services/ # Empty, add services
|
||||
├── Routes/
|
||||
│ ├── web.php # Basic index route
|
||||
│ └── api.php # If API option selected
|
||||
├── Resources/views/
|
||||
│ └── index.blade.php # Starter view
|
||||
├── Permissions.php # CRUD permissions
|
||||
├── {ModuleName}ServiceProvider.php # Auto-registered
|
||||
└── README.md # Module documentation
|
||||
```
|
||||
|
||||
## Form Options
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| **Module Name** | PascalCase name (e.g., `Accounting`, `Inventory`) |
|
||||
| **Description** | Brief description for README and config |
|
||||
| **Create Git Branch** | Auto-create `module/{name}` branch and commit |
|
||||
| **Include API Routes** | Add `Routes/api.php` with Sanctum auth |
|
||||
|
||||
## Git Integration
|
||||
|
||||
When "Create Git Branch" is enabled:
|
||||
|
||||
1. **Checks** for uncommitted changes (fails if dirty)
|
||||
2. **Creates** branch `module/{module-name}`
|
||||
3. **Generates** all module files
|
||||
4. **Commits** with message `feat: Add {ModuleName} module skeleton`
|
||||
5. **Shows** push command: `git push -u origin module/{name}`
|
||||
|
||||
### Requirements
|
||||
|
||||
- Working directory must be clean (no uncommitted changes)
|
||||
- Git must be installed in the container
|
||||
- Repository must be initialized
|
||||
|
||||
## After Generation
|
||||
|
||||
1. **Run migrations** (if you add any):
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
2. **Seed permissions**:
|
||||
```bash
|
||||
php artisan db:seed --class=RolePermissionSeeder
|
||||
```
|
||||
|
||||
3. **Clear caches**:
|
||||
```bash
|
||||
php artisan optimize:clear
|
||||
```
|
||||
|
||||
4. **Push branch** (if Git was used):
|
||||
```bash
|
||||
git push -u origin module/{name}
|
||||
```
|
||||
|
||||
## Adding Models
|
||||
|
||||
After generating the skeleton, add models manually:
|
||||
|
||||
```php
|
||||
// app/Modules/Accounting/Models/Invoice.php
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Accounting\Models;
|
||||
|
||||
use App\Traits\ModuleAuditable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
|
||||
class Invoice extends Model implements Auditable
|
||||
{
|
||||
use ModuleAuditable;
|
||||
|
||||
protected $fillable = [
|
||||
'number',
|
||||
'customer_id',
|
||||
'total',
|
||||
'status',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Filament Resources
|
||||
|
||||
Create admin CRUD in `Filament/Resources/`:
|
||||
|
||||
```php
|
||||
// app/Modules/Accounting/Filament/Resources/InvoiceResource.php
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Accounting\Filament\Resources;
|
||||
|
||||
use App\Modules\Accounting\Models\Invoice;
|
||||
use Filament\Resources\Resource;
|
||||
// ... standard Filament resource implementation
|
||||
```
|
||||
|
||||
## Comparison: UI vs CLI
|
||||
|
||||
| Feature | UI Generator | `make:module` CLI |
|
||||
|---------|--------------|-------------------|
|
||||
| Skeleton only | ✅ | ❌ (includes model) |
|
||||
| Git integration | ✅ Auto-branch | ❌ Manual |
|
||||
| Model generation | ❌ | ✅ With `--model=` |
|
||||
| Filament resource | ❌ | ✅ With `--model=` |
|
||||
| Visual feedback | ✅ | Terminal output |
|
||||
| Environment | Local only | Any |
|
||||
|
||||
**Use UI Generator when**: You need a blank canvas for complex modules with multiple models.
|
||||
|
||||
**Use CLI when**: You want a quick single-model module with all files generated.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Git working directory has uncommitted changes"
|
||||
|
||||
Commit or stash your changes first:
|
||||
```bash
|
||||
git add . && git commit -m "WIP"
|
||||
# or
|
||||
git stash
|
||||
```
|
||||
|
||||
### Module not showing in admin
|
||||
|
||||
Clear caches:
|
||||
```bash
|
||||
php artisan optimize:clear
|
||||
```
|
||||
|
||||
### Permissions not working
|
||||
|
||||
Re-seed permissions:
|
||||
```bash
|
||||
php artisan db:seed --class=RolePermissionSeeder
|
||||
```
|
||||
186
src/app/Filament/Pages/ModuleGenerator.php
Normal file
186
src/app/Filament/Pages/ModuleGenerator.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Services\ModuleDiscoveryService;
|
||||
use App\Services\ModuleGeneratorService;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ModuleGenerator extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-cube';
|
||||
protected static ?string $navigationLabel = 'Module Generator';
|
||||
protected static ?string $navigationGroup = 'Development';
|
||||
protected static ?int $navigationSort = 100;
|
||||
protected static string $view = 'filament.pages.module-generator';
|
||||
|
||||
public ?array $data = [];
|
||||
public ?array $result = null;
|
||||
public array $modules = [];
|
||||
public array $summary = [];
|
||||
public ?string $expandedModule = null;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
// Only available in local environment
|
||||
return app()->environment('local');
|
||||
}
|
||||
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
return app()->environment('local');
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'create_git_branch' => true,
|
||||
'include_api' => false,
|
||||
]);
|
||||
|
||||
$this->loadModules();
|
||||
}
|
||||
|
||||
public function loadModules(): void
|
||||
{
|
||||
$discovery = new ModuleDiscoveryService();
|
||||
$this->modules = $discovery->discoverModules();
|
||||
$this->summary = $discovery->getModuleSummary();
|
||||
}
|
||||
|
||||
public function toggleModule(string $moduleName): void
|
||||
{
|
||||
$this->expandedModule = $this->expandedModule === $moduleName ? null : $moduleName;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Module Details')
|
||||
->description('Create a new module skeleton with all the necessary folders and files.')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Module Name')
|
||||
->placeholder('e.g., Accounting, Inventory, HumanResources')
|
||||
->helperText('Use PascalCase. This will create app/Modules/YourModuleName/')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->rules(['regex:/^[A-Z][a-zA-Z0-9]*$/'])
|
||||
->validationMessages([
|
||||
'regex' => 'Module name must be PascalCase (start with uppercase, letters and numbers only).',
|
||||
])
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) =>
|
||||
$set('name', Str::studly($state))
|
||||
),
|
||||
|
||||
Textarea::make('description')
|
||||
->label('Description')
|
||||
->placeholder('Brief description of what this module does...')
|
||||
->rows(2)
|
||||
->maxLength(255),
|
||||
]),
|
||||
|
||||
Section::make('Options')
|
||||
->schema([
|
||||
Toggle::make('create_git_branch')
|
||||
->label('Create Git Branch')
|
||||
->helperText('Automatically create and checkout a new branch: module/{module-name}')
|
||||
->default(true),
|
||||
|
||||
Toggle::make('include_api')
|
||||
->label('Include API Routes')
|
||||
->helperText('Add Routes/api.php with Sanctum authentication')
|
||||
->default(false),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('What Will Be Created')
|
||||
->schema([
|
||||
\Filament\Forms\Components\Placeholder::make('structure')
|
||||
->label('')
|
||||
->content(fn () => new \Illuminate\Support\HtmlString('
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono bg-gray-50 dark:bg-gray-900 p-4 rounded-lg">
|
||||
<div class="font-bold mb-2">app/Modules/{ModuleName}/</div>
|
||||
<div class="ml-4">
|
||||
├── Config/{module_name}.php<br>
|
||||
├── Database/Migrations/<br>
|
||||
├── Database/Seeders/<br>
|
||||
├── Filament/Resources/<br>
|
||||
├── Http/Controllers/{ModuleName}Controller.php<br>
|
||||
├── Http/Middleware/<br>
|
||||
├── Http/Requests/<br>
|
||||
├── Models/<br>
|
||||
├── Policies/<br>
|
||||
├── Services/<br>
|
||||
├── Routes/web.php<br>
|
||||
├── Resources/views/index.blade.php<br>
|
||||
├── Permissions.php<br>
|
||||
├── {ModuleName}ServiceProvider.php<br>
|
||||
└── README.md
|
||||
</div>
|
||||
</div>
|
||||
')),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
public function generate(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
$service = new ModuleGeneratorService();
|
||||
$this->result = $service->generate($data['name'], [
|
||||
'description' => $data['description'] ?? '',
|
||||
'create_git_branch' => $data['create_git_branch'] ?? true,
|
||||
'include_api' => $data['include_api'] ?? false,
|
||||
]);
|
||||
|
||||
if ($this->result['success']) {
|
||||
Notification::make()
|
||||
->title('Module Created Successfully!')
|
||||
->body("Module {$this->result['module_name']} has been created.")
|
||||
->success()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
// Reset form for next module
|
||||
$this->form->fill([
|
||||
'name' => '',
|
||||
'description' => '',
|
||||
'create_git_branch' => true,
|
||||
'include_api' => false,
|
||||
]);
|
||||
|
||||
// Reload modules list
|
||||
$this->loadModules();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title('Module Creation Failed')
|
||||
->body($this->result['message'])
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function clearResult(): void
|
||||
{
|
||||
$this->result = null;
|
||||
}
|
||||
}
|
||||
248
src/app/Services/ModuleDiscoveryService.php
Normal file
248
src/app/Services/ModuleDiscoveryService.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ModuleDiscoveryService
|
||||
{
|
||||
public function discoverModules(): array
|
||||
{
|
||||
$modulesPath = app_path('Modules');
|
||||
|
||||
if (!File::exists($modulesPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$modules = [];
|
||||
$directories = File::directories($modulesPath);
|
||||
|
||||
foreach ($directories as $directory) {
|
||||
$moduleName = basename($directory);
|
||||
|
||||
// Skip non-module directories (like README files)
|
||||
if (!File::exists("{$directory}/{$moduleName}ServiceProvider.php")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modules[] = $this->getModuleInfo($moduleName, $directory);
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
|
||||
protected function getModuleInfo(string $name, string $path): array
|
||||
{
|
||||
$kebabName = Str::kebab($name);
|
||||
$snakeName = Str::snake($name);
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'slug' => $kebabName,
|
||||
'path' => $path,
|
||||
'config' => $this->getConfig($path, $snakeName),
|
||||
'models' => $this->getModels($path),
|
||||
'views' => $this->getViews($path),
|
||||
'routes' => $this->getRoutes($path, $kebabName),
|
||||
'migrations' => $this->getMigrations($path),
|
||||
'filament_resources' => $this->getFilamentResources($path),
|
||||
'permissions' => $this->getPermissions($path),
|
||||
'has_api' => File::exists("{$path}/Routes/api.php"),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getConfig(string $path, string $snakeName): array
|
||||
{
|
||||
$configPath = "{$path}/Config/{$snakeName}.php";
|
||||
|
||||
if (File::exists($configPath)) {
|
||||
return require $configPath;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getModels(string $path): array
|
||||
{
|
||||
$modelsPath = "{$path}/Models";
|
||||
|
||||
if (!File::exists($modelsPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$models = [];
|
||||
$files = File::files($modelsPath);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->getExtension() === 'php' && $file->getFilename() !== '.gitkeep') {
|
||||
$modelName = $file->getFilenameWithoutExtension();
|
||||
$models[] = [
|
||||
'name' => $modelName,
|
||||
'file' => $file->getFilename(),
|
||||
'path' => $file->getPathname(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
protected function getViews(string $path): array
|
||||
{
|
||||
$viewsPath = "{$path}/Resources/views";
|
||||
|
||||
if (!File::exists($viewsPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->scanViewsRecursive($viewsPath, '');
|
||||
}
|
||||
|
||||
protected function scanViewsRecursive(string $basePath, string $prefix): array
|
||||
{
|
||||
$views = [];
|
||||
$items = File::files($basePath);
|
||||
|
||||
foreach ($items as $file) {
|
||||
if ($file->getExtension() === 'php' && Str::endsWith($file->getFilename(), '.blade.php')) {
|
||||
$viewName = Str::replaceLast('.blade.php', '', $file->getFilename());
|
||||
$fullName = $prefix ? "{$prefix}.{$viewName}" : $viewName;
|
||||
$views[] = [
|
||||
'name' => $fullName,
|
||||
'file' => $file->getFilename(),
|
||||
'path' => $file->getPathname(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Scan subdirectories
|
||||
$directories = File::directories($basePath);
|
||||
foreach ($directories as $dir) {
|
||||
$dirName = basename($dir);
|
||||
$subPrefix = $prefix ? "{$prefix}.{$dirName}" : $dirName;
|
||||
$views = array_merge($views, $this->scanViewsRecursive($dir, $subPrefix));
|
||||
}
|
||||
|
||||
return $views;
|
||||
}
|
||||
|
||||
protected function getRoutes(string $path, string $kebabName): array
|
||||
{
|
||||
$routes = [];
|
||||
|
||||
// Get web routes
|
||||
$webRoutesPath = "{$path}/Routes/web.php";
|
||||
if (File::exists($webRoutesPath)) {
|
||||
$routes['web'] = $this->parseRouteFile($webRoutesPath, $kebabName);
|
||||
}
|
||||
|
||||
// Get API routes
|
||||
$apiRoutesPath = "{$path}/Routes/api.php";
|
||||
if (File::exists($apiRoutesPath)) {
|
||||
$routes['api'] = $this->parseRouteFile($apiRoutesPath, "api/{$kebabName}");
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
protected function parseRouteFile(string $path, string $prefix): array
|
||||
{
|
||||
$content = File::get($path);
|
||||
$routes = [];
|
||||
|
||||
// Parse Route::get, Route::post, etc.
|
||||
preg_match_all(
|
||||
"/Route::(get|post|put|patch|delete|any)\s*\(\s*['\"]([^'\"]*)['\"].*?->name\s*\(\s*['\"]([^'\"]*)['\"]|Route::(get|post|put|patch|delete|any)\s*\(\s*['\"]([^'\"]*)['\"].*?\[.*?::class,\s*['\"](\w+)['\"]\]/",
|
||||
$content,
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$method = strtoupper($match[1] ?: $match[4]);
|
||||
$uri = $match[2] ?: $match[5];
|
||||
$fullUri = $uri === '/' ? "/{$prefix}" : "/{$prefix}{$uri}";
|
||||
|
||||
$routes[] = [
|
||||
'method' => $method,
|
||||
'uri' => $fullUri,
|
||||
'name' => $match[3] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
protected function getMigrations(string $path): array
|
||||
{
|
||||
$migrationsPath = "{$path}/Database/Migrations";
|
||||
|
||||
if (!File::exists($migrationsPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$migrations = [];
|
||||
$files = File::files($migrationsPath);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->getExtension() === 'php') {
|
||||
$migrations[] = [
|
||||
'name' => $file->getFilename(),
|
||||
'path' => $file->getPathname(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
protected function getFilamentResources(string $path): array
|
||||
{
|
||||
$resourcesPath = "{$path}/Filament/Resources";
|
||||
|
||||
if (!File::exists($resourcesPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resources = [];
|
||||
$files = File::files($resourcesPath);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->getExtension() === 'php' && Str::endsWith($file->getFilename(), 'Resource.php')) {
|
||||
$resourceName = Str::replaceLast('Resource.php', '', $file->getFilename());
|
||||
$resources[] = [
|
||||
'name' => $resourceName,
|
||||
'file' => $file->getFilename(),
|
||||
'path' => $file->getPathname(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
protected function getPermissions(string $path): array
|
||||
{
|
||||
$permissionsPath = "{$path}/Permissions.php";
|
||||
|
||||
if (File::exists($permissionsPath)) {
|
||||
return require $permissionsPath;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getModuleSummary(): array
|
||||
{
|
||||
$modules = $this->discoverModules();
|
||||
|
||||
return [
|
||||
'total' => count($modules),
|
||||
'with_models' => count(array_filter($modules, fn($m) => !empty($m['models']))),
|
||||
'with_filament' => count(array_filter($modules, fn($m) => !empty($m['filament_resources']))),
|
||||
'with_api' => count(array_filter($modules, fn($m) => $m['has_api'])),
|
||||
];
|
||||
}
|
||||
}
|
||||
492
src/app/Services/ModuleGeneratorService.php
Normal file
492
src/app/Services/ModuleGeneratorService.php
Normal file
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use CzProject\GitPhp\Git;
|
||||
use CzProject\GitPhp\GitRepository;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ModuleGeneratorService
|
||||
{
|
||||
protected string $studlyName;
|
||||
protected string $kebabName;
|
||||
protected string $snakeName;
|
||||
protected string $modulePath;
|
||||
protected array $logs = [];
|
||||
protected ?GitRepository $repo = null;
|
||||
|
||||
public function generate(string $name, array $options = []): array
|
||||
{
|
||||
$this->studlyName = Str::studly($name);
|
||||
$this->kebabName = Str::kebab($name);
|
||||
$this->snakeName = Str::snake($name);
|
||||
$this->modulePath = app_path("Modules/{$this->studlyName}");
|
||||
|
||||
$options = array_merge([
|
||||
'description' => '',
|
||||
'create_git_branch' => true,
|
||||
'include_api' => false,
|
||||
], $options);
|
||||
|
||||
// Check if module already exists
|
||||
if (File::exists($this->modulePath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Module {$this->studlyName} already exists!",
|
||||
'logs' => $this->logs,
|
||||
];
|
||||
}
|
||||
|
||||
// Handle Git branching
|
||||
$branchName = null;
|
||||
if ($options['create_git_branch']) {
|
||||
$gitResult = $this->createGitBranch();
|
||||
if (!$gitResult['success']) {
|
||||
return $gitResult;
|
||||
}
|
||||
$branchName = $gitResult['branch'];
|
||||
}
|
||||
|
||||
// Generate module skeleton
|
||||
$this->createDirectoryStructure();
|
||||
$this->createServiceProvider($options['description']);
|
||||
$this->createConfig($options['description']);
|
||||
$this->createPermissions();
|
||||
$this->createController();
|
||||
$this->createRoutes($options['include_api']);
|
||||
$this->createViews();
|
||||
$this->createReadme($options['description']);
|
||||
$this->registerServiceProvider();
|
||||
|
||||
// Git commit
|
||||
if ($options['create_git_branch'] && $this->repo) {
|
||||
$this->commitChanges();
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Module {$this->studlyName} created successfully!",
|
||||
'module_name' => $this->studlyName,
|
||||
'module_path' => $this->modulePath,
|
||||
'branch' => $branchName,
|
||||
'logs' => $this->logs,
|
||||
'next_steps' => $this->getNextSteps($branchName),
|
||||
];
|
||||
}
|
||||
|
||||
protected function createGitBranch(): array
|
||||
{
|
||||
try {
|
||||
$git = new Git();
|
||||
$repoPath = base_path();
|
||||
|
||||
// Check if we're in a git repository
|
||||
if (!File::exists($repoPath . '/.git')) {
|
||||
$this->log('⚠ No Git repository found, skipping branch creation');
|
||||
return ['success' => true, 'branch' => null];
|
||||
}
|
||||
|
||||
$this->repo = $git->open($repoPath);
|
||||
|
||||
// Check for uncommitted changes
|
||||
if ($this->repo->hasChanges()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Git working directory has uncommitted changes. Please commit or stash them first.',
|
||||
'logs' => $this->logs,
|
||||
];
|
||||
}
|
||||
|
||||
$branchName = "module/{$this->kebabName}";
|
||||
|
||||
// Create and checkout new branch
|
||||
$this->repo->createBranch($branchName, true);
|
||||
$this->log("✓ Created and checked out branch: {$branchName}");
|
||||
|
||||
return ['success' => true, 'branch' => $branchName];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Git error: ' . $e->getMessage(),
|
||||
'logs' => $this->logs,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function commitChanges(): void
|
||||
{
|
||||
try {
|
||||
$this->repo->addAllChanges();
|
||||
$this->repo->commit("feat: Add {$this->studlyName} module skeleton");
|
||||
$this->log("✓ Committed changes to Git");
|
||||
} catch (\Exception $e) {
|
||||
$this->log("⚠ Git commit failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDirectoryStructure(): void
|
||||
{
|
||||
$directories = [
|
||||
'',
|
||||
'/Config',
|
||||
'/Database/Migrations',
|
||||
'/Database/Seeders',
|
||||
'/Filament/Resources',
|
||||
'/Http/Controllers',
|
||||
'/Http/Middleware',
|
||||
'/Http/Requests',
|
||||
'/Models',
|
||||
'/Policies',
|
||||
'/Services',
|
||||
'/Routes',
|
||||
'/Resources/views',
|
||||
];
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
File::makeDirectory("{$this->modulePath}{$dir}", 0755, true);
|
||||
}
|
||||
|
||||
// Create .gitkeep files in empty directories
|
||||
$emptyDirs = [
|
||||
'/Database/Migrations',
|
||||
'/Database/Seeders',
|
||||
'/Filament/Resources',
|
||||
'/Http/Middleware',
|
||||
'/Http/Requests',
|
||||
'/Models',
|
||||
'/Policies',
|
||||
'/Services',
|
||||
];
|
||||
|
||||
foreach ($emptyDirs as $dir) {
|
||||
File::put("{$this->modulePath}{$dir}/.gitkeep", '');
|
||||
}
|
||||
|
||||
$this->log("✓ Created directory structure");
|
||||
}
|
||||
|
||||
protected function createServiceProvider(string $description): void
|
||||
{
|
||||
$stub = <<<PHP
|
||||
<?php
|
||||
|
||||
namespace App\Modules\\{$this->studlyName};
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class {$this->studlyName}ServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
\$this->mergeConfigFrom(
|
||||
__DIR__ . '/Config/{$this->snakeName}.php',
|
||||
'{$this->snakeName}'
|
||||
);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
\$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
|
||||
\$this->loadViewsFrom(__DIR__ . '/Resources/views', '{$this->kebabName}');
|
||||
|
||||
\$this->registerRoutes();
|
||||
\$this->registerPermissions();
|
||||
}
|
||||
|
||||
protected function registerRoutes(): void
|
||||
{
|
||||
Route::middleware(['web', 'auth'])
|
||||
->prefix('{$this->kebabName}')
|
||||
->name('{$this->kebabName}.')
|
||||
->group(__DIR__ . '/Routes/web.php');
|
||||
|
||||
if (file_exists(__DIR__ . '/Routes/api.php')) {
|
||||
Route::middleware(['api', 'auth:sanctum'])
|
||||
->prefix('api/{$this->kebabName}')
|
||||
->name('api.{$this->kebabName}.')
|
||||
->group(__DIR__ . '/Routes/api.php');
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerPermissions(): void
|
||||
{
|
||||
// Permissions are registered via RolePermissionSeeder
|
||||
// See: Permissions.php in this module
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
File::put("{$this->modulePath}/{$this->studlyName}ServiceProvider.php", $stub);
|
||||
$this->log("✓ Created ServiceProvider");
|
||||
}
|
||||
|
||||
protected function createConfig(string $description): void
|
||||
{
|
||||
$stub = <<<PHP
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => '{$this->studlyName}',
|
||||
'slug' => '{$this->kebabName}',
|
||||
'description' => '{$description}',
|
||||
'version' => '1.0.0',
|
||||
|
||||
'audit' => [
|
||||
'enabled' => true,
|
||||
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
|
||||
'include' => [],
|
||||
'exclude' => [],
|
||||
],
|
||||
];
|
||||
PHP;
|
||||
|
||||
File::put("{$this->modulePath}/Config/{$this->snakeName}.php", $stub);
|
||||
$this->log("✓ Created Config");
|
||||
}
|
||||
|
||||
protected function createPermissions(): void
|
||||
{
|
||||
$stub = <<<PHP
|
||||
<?php
|
||||
|
||||
return [
|
||||
'{$this->snakeName}.view' => 'View {$this->studlyName}',
|
||||
'{$this->snakeName}.create' => 'Create {$this->studlyName} records',
|
||||
'{$this->snakeName}.edit' => 'Edit {$this->studlyName} records',
|
||||
'{$this->snakeName}.delete' => 'Delete {$this->studlyName} records',
|
||||
];
|
||||
PHP;
|
||||
|
||||
File::put("{$this->modulePath}/Permissions.php", $stub);
|
||||
$this->log("✓ Created Permissions");
|
||||
}
|
||||
|
||||
protected function createController(): void
|
||||
{
|
||||
$stub = <<<PHP
|
||||
<?php
|
||||
|
||||
namespace App\Modules\\{$this->studlyName}\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class {$this->studlyName}Controller extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
\$this->authorize('{$this->snakeName}.view');
|
||||
|
||||
return view('{$this->kebabName}::index');
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
File::put("{$this->modulePath}/Http/Controllers/{$this->studlyName}Controller.php", $stub);
|
||||
$this->log("✓ Created Controller");
|
||||
}
|
||||
|
||||
protected function createRoutes(bool $includeApi): void
|
||||
{
|
||||
// Web routes
|
||||
$webStub = <<<PHP
|
||||
<?php
|
||||
|
||||
use App\Modules\\{$this->studlyName}\Http\Controllers\\{$this->studlyName}Controller;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', [{$this->studlyName}Controller::class, 'index'])->name('index');
|
||||
|
||||
// Add more routes here:
|
||||
// Route::get('/create', [{$this->studlyName}Controller::class, 'create'])->name('create');
|
||||
// Route::post('/', [{$this->studlyName}Controller::class, 'store'])->name('store');
|
||||
// Route::get('/{id}', [{$this->studlyName}Controller::class, 'show'])->name('show');
|
||||
// Route::get('/{id}/edit', [{$this->studlyName}Controller::class, 'edit'])->name('edit');
|
||||
// Route::put('/{id}', [{$this->studlyName}Controller::class, 'update'])->name('update');
|
||||
// Route::delete('/{id}', [{$this->studlyName}Controller::class, 'destroy'])->name('destroy');
|
||||
PHP;
|
||||
|
||||
File::put("{$this->modulePath}/Routes/web.php", $webStub);
|
||||
|
||||
// API routes (if requested)
|
||||
if ($includeApi) {
|
||||
$apiStub = <<<PHP
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
// API routes here
|
||||
// Route::get('/', fn() => response()->json(['message' => '{$this->studlyName} API']));
|
||||
});
|
||||
PHP;
|
||||
|
||||
File::put("{$this->modulePath}/Routes/api.php", $apiStub);
|
||||
$this->log("✓ Created Routes (web + api)");
|
||||
} else {
|
||||
$this->log("✓ Created Routes (web)");
|
||||
}
|
||||
}
|
||||
|
||||
protected function createViews(): void
|
||||
{
|
||||
$stub = <<<BLADE
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('{$this->studlyName}') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<h3 class="text-lg font-medium mb-4">{{ __('{$this->studlyName} Module') }}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
This is your new module. Start building by adding models, controllers, and views.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<h4 class="font-medium mb-2">Quick Start:</h4>
|
||||
<ul class="list-disc list-inside text-sm space-y-1">
|
||||
<li>Add models in <code>app/Modules/{$this->studlyName}/Models/</code></li>
|
||||
<li>Add migrations in <code>app/Modules/{$this->studlyName}/Database/Migrations/</code></li>
|
||||
<li>Add Filament resources in <code>app/Modules/{$this->studlyName}/Filament/Resources/</code></li>
|
||||
<li>Extend routes in <code>app/Modules/{$this->studlyName}/Routes/web.php</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
BLADE;
|
||||
|
||||
File::put("{$this->modulePath}/Resources/views/index.blade.php", $stub);
|
||||
$this->log("✓ Created Views");
|
||||
}
|
||||
|
||||
protected function createReadme(string $description): void
|
||||
{
|
||||
$stub = <<<MD
|
||||
# {$this->studlyName} Module
|
||||
|
||||
{$description}
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
{$this->studlyName}/
|
||||
├── Config/{$this->snakeName}.php # Module configuration
|
||||
├── Database/
|
||||
│ ├── Migrations/ # Database migrations
|
||||
│ └── Seeders/ # Database seeders
|
||||
├── Filament/Resources/ # Admin panel resources
|
||||
├── Http/
|
||||
│ ├── Controllers/ # HTTP controllers
|
||||
│ ├── Middleware/ # Module middleware
|
||||
│ └── Requests/ # Form requests
|
||||
├── Models/ # Eloquent models
|
||||
├── Policies/ # Authorization policies
|
||||
├── Services/ # Business logic services
|
||||
├── Routes/
|
||||
│ ├── web.php # Web routes
|
||||
│ └── api.php # API routes (if enabled)
|
||||
├── Resources/views/ # Blade templates
|
||||
├── Permissions.php # Module permissions
|
||||
├── {$this->studlyName}ServiceProvider.php
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `{$this->snakeName}.view` | View {$this->studlyName} |
|
||||
| `{$this->snakeName}.create` | Create records |
|
||||
| `{$this->snakeName}.edit` | Edit records |
|
||||
| `{$this->snakeName}.delete` | Delete records |
|
||||
|
||||
## Routes
|
||||
|
||||
| Method | URI | Name | Description |
|
||||
|--------|-----|------|-------------|
|
||||
| GET | `/{$this->kebabName}` | `{$this->kebabName}.index` | Module index |
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Add a Model:**
|
||||
```bash
|
||||
# Create model manually or use artisan
|
||||
php artisan make:model Modules/{$this->studlyName}/Models/YourModel -m
|
||||
```
|
||||
|
||||
2. **Run Migrations:**
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
3. **Seed Permissions:**
|
||||
```bash
|
||||
php artisan db:seed --class=RolePermissionSeeder
|
||||
```
|
||||
|
||||
4. **Add Filament Resource:**
|
||||
Create resources in `Filament/Resources/` following the Filament documentation.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `Config/{$this->snakeName}.php` to customize module settings including audit behavior.
|
||||
MD;
|
||||
|
||||
File::put("{$this->modulePath}/README.md", $stub);
|
||||
$this->log("✓ Created README.md");
|
||||
}
|
||||
|
||||
protected function registerServiceProvider(): void
|
||||
{
|
||||
$providersPath = base_path('bootstrap/providers.php');
|
||||
|
||||
if (File::exists($providersPath)) {
|
||||
$content = File::get($providersPath);
|
||||
$providerClass = "App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class";
|
||||
|
||||
if (!str_contains($content, $providerClass)) {
|
||||
$content = preg_replace(
|
||||
'/(\];)/',
|
||||
" {$providerClass},\n$1",
|
||||
$content
|
||||
);
|
||||
File::put($providersPath, $content);
|
||||
$this->log("✓ Registered ServiceProvider");
|
||||
}
|
||||
} else {
|
||||
$this->log("⚠ Could not auto-register ServiceProvider");
|
||||
}
|
||||
}
|
||||
|
||||
protected function getNextSteps(?string $branchName): array
|
||||
{
|
||||
$steps = [
|
||||
"Run migrations: `php artisan migrate`",
|
||||
"Seed permissions: `php artisan db:seed --class=RolePermissionSeeder`",
|
||||
"Clear caches: `php artisan optimize:clear`",
|
||||
"Access frontend: `/{$this->kebabName}`",
|
||||
];
|
||||
|
||||
if ($branchName) {
|
||||
$steps[] = "Push branch: `git push -u origin {$branchName}`";
|
||||
$steps[] = "Create merge request when ready";
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
protected function log(string $message): void
|
||||
{
|
||||
$this->logs[] = $message;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"spatie/laravel-permission": "^6.24"
|
||||
},
|
||||
"require-dev": {
|
||||
"czproject/git-php": "^4.6",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/breeze": "^2.3",
|
||||
"laravel/pail": "^1.1",
|
||||
|
||||
66
src/composer.lock
generated
66
src/composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1f84406b8f4e254677813bb043dd37c3",
|
||||
"content-hash": "384bae7218ddf4c1e2b18a8c99946102",
|
||||
"packages": [
|
||||
{
|
||||
"name": "anourvalar/eloquent-serialize",
|
||||
@@ -8305,6 +8305,70 @@
|
||||
],
|
||||
"time": "2026-01-08T08:02:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "czproject/git-php",
|
||||
"version": "v4.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/czproject/git-php.git",
|
||||
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
|
||||
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "8.0 - 8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Pecha",
|
||||
"email": "janpecha@email.cz"
|
||||
}
|
||||
],
|
||||
"description": "Library for work with Git repository in PHP.",
|
||||
"keywords": [
|
||||
"git"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/czproject/git-php/issues",
|
||||
"source": "https://github.com/czproject/git-php/tree/v4.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/janpecha",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.janpecha.cz/donate/git-php/",
|
||||
"type": "other"
|
||||
},
|
||||
{
|
||||
"url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA",
|
||||
"type": "stripe"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/czproject",
|
||||
"type": "thanks.dev"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-10T07:24:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.24.1",
|
||||
|
||||
315
src/resources/views/filament/pages/module-generator.blade.php
Normal file
315
src/resources/views/filament/pages/module-generator.blade.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
{{-- Warning Banner --}}
|
||||
<div class="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Development Only:</strong> This tool is only available in the local environment.
|
||||
It creates module skeletons and optionally manages Git branches.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Registered Modules Section --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<x-heroicon-o-cube-transparent class="w-5 h-5" />
|
||||
Registered Modules
|
||||
</h3>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded">
|
||||
{{ $summary['total'] ?? 0 }} Total
|
||||
</span>
|
||||
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded">
|
||||
{{ $summary['with_models'] ?? 0 }} with Models
|
||||
</span>
|
||||
<span class="px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded">
|
||||
{{ $summary['with_filament'] ?? 0 }} with Filament
|
||||
</span>
|
||||
<span class="px-2 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded">
|
||||
{{ $summary['with_api'] ?? 0 }} with API
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(empty($modules))
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<x-heroicon-o-inbox class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No modules registered yet. Create your first module below!</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@foreach($modules as $module)
|
||||
<div class="group">
|
||||
{{-- Module Header --}}
|
||||
<button
|
||||
type="button"
|
||||
wire:click="toggleModule('{{ $module['name'] }}')"
|
||||
class="w-full p-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<x-heroicon-o-cube class="w-5 h-5 text-gray-400" />
|
||||
<div class="text-left">
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ $module['name'] }}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
/{{ $module['slug'] }}
|
||||
@if($module['has_api'])
|
||||
<span class="ml-2 text-xs px-1.5 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 rounded">API</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex gap-2 text-xs">
|
||||
@if(count($module['models']) > 0)
|
||||
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded">
|
||||
{{ count($module['models']) }} {{ Str::plural('Model', count($module['models'])) }}
|
||||
</span>
|
||||
@endif
|
||||
@if(count($module['views']) > 0)
|
||||
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded">
|
||||
{{ count($module['views']) }} {{ Str::plural('View', count($module['views'])) }}
|
||||
</span>
|
||||
@endif
|
||||
@if(count($module['filament_resources']) > 0)
|
||||
<span class="px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded">
|
||||
{{ count($module['filament_resources']) }} {{ Str::plural('Resource', count($module['filament_resources'])) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<x-heroicon-o-chevron-down class="w-5 h-5 text-gray-400 transition-transform {{ $expandedModule === $module['name'] ? 'rotate-180' : '' }}" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{{-- Module Details (Expandable) --}}
|
||||
@if($expandedModule === $module['name'])
|
||||
<div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pt-4">
|
||||
{{-- Models --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<x-heroicon-o-circle-stack class="w-4 h-4" />
|
||||
Models
|
||||
</h4>
|
||||
@if(count($module['models']) > 0)
|
||||
<ul class="text-sm space-y-1">
|
||||
@foreach($module['models'] as $model)
|
||||
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $model['name'] }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No models</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Views --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
Views
|
||||
</h4>
|
||||
@if(count($module['views']) > 0)
|
||||
<ul class="text-sm space-y-1">
|
||||
@foreach($module['views'] as $view)
|
||||
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $module['slug'] }}::{{ $view['name'] }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No views</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Filament Resources --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<x-heroicon-o-rectangle-group class="w-4 h-4" />
|
||||
Filament Resources
|
||||
</h4>
|
||||
@if(count($module['filament_resources']) > 0)
|
||||
<ul class="text-sm space-y-1">
|
||||
@foreach($module['filament_resources'] as $resource)
|
||||
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $resource['name'] }}Resource</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No Filament resources</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Routes --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<x-heroicon-o-arrow-path class="w-4 h-4" />
|
||||
Routes
|
||||
</h4>
|
||||
@if(!empty($module['routes']['web']))
|
||||
<ul class="text-sm space-y-1">
|
||||
@foreach($module['routes']['web'] as $route)
|
||||
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">
|
||||
<span class="text-blue-600 dark:text-blue-400">{{ $route['method'] }}</span>
|
||||
{{ $route['uri'] }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No web routes parsed</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Migrations --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<x-heroicon-o-table-cells class="w-4 h-4" />
|
||||
Migrations
|
||||
</h4>
|
||||
@if(count($module['migrations']) > 0)
|
||||
<ul class="text-sm space-y-1">
|
||||
@foreach($module['migrations'] as $migration)
|
||||
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs truncate" title="{{ $migration['name'] }}">{{ $migration['name'] }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No migrations</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Permissions --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<x-heroicon-o-key class="w-4 h-4" />
|
||||
Permissions
|
||||
</h4>
|
||||
@if(count($module['permissions']) > 0)
|
||||
<ul class="text-sm space-y-1">
|
||||
@foreach($module['permissions'] as $key => $label)
|
||||
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $key }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No permissions</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Module Path --}}
|
||||
<div class="mt-4 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||
Path: {{ $module['path'] }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Create Module Form --}}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<x-heroicon-o-plus-circle class="w-5 h-5" />
|
||||
Create New Module
|
||||
</h3>
|
||||
|
||||
<form wire:submit="generate">
|
||||
{{ $this->form }}
|
||||
|
||||
<div class="mt-6 flex gap-3">
|
||||
<x-filament::button type="submit" size="lg">
|
||||
<x-heroicon-o-sparkles class="w-5 h-5 mr-2" />
|
||||
Generate Module
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Result Panel --}}
|
||||
@if($result)
|
||||
<div class="mt-8">
|
||||
@if($result['success'])
|
||||
<div class="p-6 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<x-heroicon-o-check-circle class="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||
<h3 class="text-lg font-semibold text-green-800 dark:text-green-200">
|
||||
Module Created Successfully!
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-green-700 dark:text-green-300">Module:</span>
|
||||
<code class="ml-2 px-2 py-1 bg-green-100 dark:bg-green-800 rounded">{{ $result['module_name'] }}</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-medium text-green-700 dark:text-green-300">Path:</span>
|
||||
<code class="ml-2 px-2 py-1 bg-green-100 dark:bg-green-800 rounded text-xs">{{ $result['module_path'] }}</code>
|
||||
</div>
|
||||
|
||||
@if($result['branch'])
|
||||
<div>
|
||||
<span class="font-medium text-green-700 dark:text-green-300">Git Branch:</span>
|
||||
<code class="ml-2 px-2 py-1 bg-green-100 dark:bg-green-800 rounded">{{ $result['branch'] }}</code>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Logs --}}
|
||||
<div class="mt-4">
|
||||
<span class="font-medium text-green-700 dark:text-green-300">Generation Log:</span>
|
||||
<div class="mt-2 p-3 bg-white dark:bg-gray-800 rounded border border-green-200 dark:border-green-700 font-mono text-xs">
|
||||
@foreach($result['logs'] as $log)
|
||||
<div class="py-0.5">{{ $log }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Next Steps --}}
|
||||
<div class="mt-4">
|
||||
<span class="font-medium text-green-700 dark:text-green-300">Next Steps:</span>
|
||||
<ol class="mt-2 list-decimal list-inside space-y-1 text-green-800 dark:text-green-200">
|
||||
@foreach($result['next_steps'] as $step)
|
||||
<li>{!! preg_replace('/`([^`]+)`/', '<code class="px-1 py-0.5 bg-green-100 dark:bg-green-800 rounded text-xs">$1</code>', e($step)) !!}</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<x-filament::button color="gray" wire:click="clearResult">
|
||||
Create Another Module
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="p-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<x-heroicon-o-x-circle class="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||
<h3 class="text-lg font-semibold text-red-800 dark:text-red-200">
|
||||
Module Creation Failed
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="text-red-700 dark:text-red-300">{{ $result['message'] }}</p>
|
||||
|
||||
@if(!empty($result['logs']))
|
||||
<div class="mt-4 p-3 bg-white dark:bg-gray-800 rounded border border-red-200 dark:border-red-700 font-mono text-xs">
|
||||
@foreach($result['logs'] as $log)
|
||||
<div class="py-0.5">{{ $log }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6">
|
||||
<x-filament::button color="gray" wire:click="clearResult">
|
||||
Try Again
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
Reference in New Issue
Block a user