diff --git a/composer.json b/composer.json index 533c492..540b2aa 100644 --- a/composer.json +++ b/composer.json @@ -37,4 +37,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 8b2dde4..b4f2ae7 100644 --- a/composer.lock +++ b/composer.lock @@ -5594,4 +5594,4 @@ }, "platform-dev": {}, "plugin-api-version": "2.6.0" -} +} \ No newline at end of file diff --git a/src/Commands/CrudGenerator.php b/src/Commands/CrudGenerator.php index ef6990d..3732a3c 100644 --- a/src/Commands/CrudGenerator.php +++ b/src/Commands/CrudGenerator.php @@ -76,8 +76,9 @@ protected function promptForMissingArgumentsUsing(): array 'tailwind' => 'Blade with Tailwind css', 'livewire' => 'Livewire with Tailwind css', 'api' => 'API only', + 'jetstream'=> 'Jetstream inertia with Tailwind css', ], - scroll: 4, + scroll: 5, ), ]; } @@ -89,6 +90,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp 'livewire' => 'livewire', 'react' => 'react', 'vue' => 'vue', + 'jetstream' => 'jetstream', default => 'bootstrap', }; } @@ -111,6 +113,11 @@ protected function writeRoute(): static 'api' => [ "Route::apiResource('".$this->_getRoute()."', {$this->name}Controller::class);", ], + 'jetstream' => [ + "Route::middleware(['auth:sanctum', 'verified'])->group(function () {", + " Route::resource('{$this->_getRoute()}', {$replacements['{{modelName}}']}" . "Controller::class);", + "});" + ], default => [ "Route::resource('".$this->_getRoute()."', {$this->name}Controller::class);", ] @@ -133,6 +140,12 @@ protected function writeRoute(): static */ protected function buildController(): static { + if($this->options['stack'] == 'jetstream') { + $this->buildJetstream(); + + return $this; + } + if ($this->options['stack'] == 'livewire') { $this->buildLivewire(); @@ -202,6 +215,222 @@ protected function buildLivewire(): void $this->write($formPath, $componentTemplate); } + protected function buildJetstream(): void + { + $this->info('Creating Jetstream Inertia Components ...'); + + $folder = ucfirst(Str::plural($this->name)); + $replace = array_merge($this->buildReplacements(), $this->modelReplacements()); + + // Generate Vue-specific replacements + $formFields = ''; + $formData = ''; + $formEditData = ''; + $detailFields = ''; + $tableHead = ''; + $tableBody = ''; + $querySearch = ''; + + $lowerModelName = strtolower($this->name); + $capitalizeModelName = lcfirst($this->name); + $isFirstColumn = true; + + foreach ($this->getColumnsWithType() as $column => $type) { + $title = Str::title(str_replace('_', ' ', $column)); + + // Generate Vue components specific fields + $tableHead .= << + $title + + + HTML; + + $tableBody .= << + {{ {$capitalizeModelName}.$column }} + + + HTML; + + $formFields .= $this->getJetstreamFormField($title, $column,$type); + $formData .= " $column: '',\n"; + $formEditData .= " $column: this.{$capitalizeModelName}.$column,\n"; + $detailFields .= $this->getJetstreamDetailField($title, $column); + if ($isFirstColumn) { + $querySearch .= "\$query->where('{$column}', 'like', \"%{\$request->search}%\")\n"; + $isFirstColumn = false; + } else { + $querySearch .= " ->orWhere('{$column}', 'like', \"%{\$request->search}%\")\n"; + } + } + + // Add to replacements + $replace['{{tableHeader}}'] = $tableHead; + $replace['{{tableBody}}'] = $tableBody; + $replace['{{formFields}}'] = $formFields; + $replace['{{formData}}'] = $formData; + $replace['{{formEditData}}'] = $formEditData; + $replace['{{detailFields}}'] = $detailFields; + $replace['{{querySearch}}'] = $querySearch; + + // Create Inertia component directory + $componentPath = resource_path("js/Pages/{$folder}"); + if (!$this->files->isDirectory($componentPath)) { + $this->files->makeDirectory($componentPath, 0755, true); + } + + $this->createSharedComponents(); + + // Generate the Inertia components + foreach (['Index', 'Create', 'Edit', 'Show'] as $component) { + $templatePath = $this->getStub("views/jetstream/{$component}", false); + + if ($this->files->exists($templatePath)) { + $content = str_replace( + array_keys($replace), + array_values($replace), + $this->getStub("views/jetstream/{$component}") + ); + + $this->write("{$componentPath}/{$component}.vue", $content); + } else { + $this->warn("Stub for {$component} not found. Skipping..."); + } + } + + // Create Controller + $controllerPath = $this->_getControllerPath($this->name); + + if ($this->files->exists($controllerPath) && $this->ask('Already exist Controller. Do you want overwrite (y/n)?', 'y') == 'n') { + return; + } + + $this->info('Creating Controller for Jetstream...'); + + $controllerTemplate = str_replace( + array_keys($replace), + array_values($replace), + $this->getStub('jetstream/Controller') + ); + + $this->write($controllerPath, $controllerTemplate); + + // Create Model + $this->buildModel(); + } + + protected function createSharedComponents(): void + { + $componentsPath = resource_path('js/Components'); + + // Check if Components directory exists, create if not + if (!$this->files->isDirectory($componentsPath)) { + $this->files->makeDirectory($componentsPath, 0755, true); + } + + // Create Pagination component + $paginationPath = $componentsPath.'/Pagination.vue'; + if (!$this->files->exists($paginationPath)) { + $this->info('Creating Pagination component...'); + $paginationContent = $this->getPaginationComponent(); + $this->write($paginationPath, $paginationContent); + } + + // Create SearchFilter component + $searchFilterPath = $componentsPath.'/SearchFilter.vue'; + if (!$this->files->exists($searchFilterPath)) { + $this->info('Creating SearchFilter component...'); + $searchFilterContent = $this->getSearchFilterComponent(); + $this->write($searchFilterPath, $searchFilterContent); + } + } + + protected function getPaginationComponent(): string + { + return << +
+
+ +
+
+ + + + VUE; + } + + protected function getSearchFilterComponent(): string + { + return << +
+
+ + + +
+ +
+ + + + VUE; + } /** * @return $this * @throws FileNotFoundException @@ -258,14 +487,28 @@ protected function buildViews(): static $tableBody = "\n"; $viewRows = "\n"; $form = "\n"; - - foreach ($this->getFilteredColumns() as $column) { + + // Add variables for Jetstream Vue components + $formFields = "\n"; + $formData = "\n"; + $formEditData = "\n"; + $detailFields = "\n"; + + foreach ($this->getColumnsWithType() as $column => $type) { + $title = Str::title(str_replace('_', ' ', $column)); $tableHead .= $this->getHead($title); $tableBody .= $this->getBody($column); - $viewRows .= $this->getField($title, $column, 'view-field'); - $form .= $this->getField($title, $column); + if ($this->options['stack'] != 'jetstream') { + $viewRows .= $this->getField($title, $column, 'view-field'); + $form .= $this->getField($title, $column); + } else { + $formFields .= $this->getJetstreamFormField($title, $column,$type); + $formData .= "\t\t\t\t$column: '',\n"; + $formEditData .= "\t\t\t\t$column: this.{$this->name}.$column,\n"; + $detailFields .= $this->getJetstreamDetailField($title, $column); + } } $replace = array_merge($this->buildReplacements(), [ @@ -273,10 +516,18 @@ protected function buildViews(): static '{{tableBody}}' => $tableBody, '{{viewRows}}' => $viewRows, '{{form}}' => $form, + '{{formFields}}' => $formFields, + '{{formData}}' => $formData, + '{{formEditData}}' => $formEditData, + '{{detailFields}}' => $detailFields, ]); $this->buildLayout(); + if ($this->options['stack'] === 'jetstream') { + return $this; + } + foreach (['index', 'create', 'edit', 'form', 'show'] as $view) { $path = match ($this->options['stack']) { 'livewire' => $this->isLaravel12() ? "views/{$this->options['stack']}/12/$view" : "views/{$this->options['stack']}/default/$view", @@ -302,4 +553,53 @@ private function _buildClassName(): string { return Str::studly(Str::singular($this->table)); } + + protected function mapColumnTypeToInputType(string $type): string + { + return match ($type) { + 'int','integer', 'bigint', 'smallint', 'tinyint' => 'number', + 'float', 'double', 'decimal' => 'number', + 'boolean' => 'checkbox', + 'date' => 'date', + 'datetime', 'timestamp' => 'datetime-local', + 'time' => 'time', + 'email' => 'email', + 'varchar', 'string' => 'text', + 'text', 'json'=> 'textarea', + default => 'text', + }; + } + + protected function getJetstreamFormField(string $title, string $column, string $type_column=""): string + { + $inputType = $this->mapColumnTypeToInputType(strtolower($type_column)); + + return << + + +
{{ form.errors.$column }}
+ + HTML; + } + + protected function getJetstreamDetailField(string $title, string $column): string + { + $capitalizeModelName = lcfirst($this->name); + return << +

$title:

+

{{ {$capitalizeModelName}.$column }}

+ + + HTML; + } } diff --git a/src/Commands/GeneratorCommand.php b/src/Commands/GeneratorCommand.php index 17340c1..23ac5ea 100644 --- a/src/Commands/GeneratorCommand.php +++ b/src/Commands/GeneratorCommand.php @@ -276,6 +276,7 @@ protected function _getViewPath(string $view): string $name = Str::kebab($this->name); $path = match ($this->options['stack']) { 'livewire' => "/views/livewire/$name/$view.blade.php", + 'jetstream' => "/js/Pages/" . Str::studly(Str::plural($this->name)) . "/$view.vue", default => "/views/$name/$view.blade.php" }; @@ -407,11 +408,17 @@ protected function buildLayout(): void $uiPackage = match ($this->options['stack']) { 'tailwind', 'react', 'vue' => 'laravel/breeze', 'livewire' => $this->isLaravel12() ? 'laravel/livewire-starter-kit' : 'laravel/breeze', + 'jetstream' => 'laravel/jetstream', default => 'laravel/ui' }; - if (! $this->requireComposerPackages([$uiPackage], true)) { - throw new Exception("Unable to install $uiPackage. Please install it manually"); + if (\Composer\InstalledVersions::isInstalled($uiPackage)) { + $this->info("$uiPackage is already installed, skipping installation."); + return ; + } else { + if (! $this->requireComposerPackages([$uiPackage], true)) { + throw new Exception("Unable to install $uiPackage. Please install it manually"); + } } $uiCommand = match ($this->options['stack']) { @@ -419,13 +426,14 @@ protected function buildLayout(): void 'livewire' => 'php artisan breeze:install livewire', 'react' => 'php artisan breeze:install react', 'vue' => 'php artisan breeze:install vue', + 'jetstream' => 'php artisan jetstream:install inertia', default => 'php artisan ui bootstrap --auth' }; // Do not run command for v12.* - if ($this->isLaravel12()) { - return; - } + // if ($this->isLaravel12()) { + // return; + // } $this->runCommands([$uiCommand]); } @@ -461,6 +469,20 @@ protected function getFilteredColumns(): array }); } + protected function getColumnsWithType(): array + { + $unwanted = $this->unwantedColumns; + $columns = []; + + foreach ($this->getColumns() as $column) { + $columns[$column['name']] = $column['type_name']; + } + + return array_filter($columns, function ($type, $name) use ($unwanted) { + return ! in_array($name, $unwanted); + }, ARRAY_FILTER_USE_BOTH); + } + /** * Make model attributes/replacements. * diff --git a/src/stubs/jetstream/Controller.stub b/src/stubs/jetstream/Controller.stub new file mode 100644 index 0000000..dc7e12d --- /dev/null +++ b/src/stubs/jetstream/Controller.stub @@ -0,0 +1,110 @@ + $request->only('search'), + '{{modelNamePluralLowerCase}}' => {{modelName}}::query() + ->when($request->filled('search'), function($query, $search) use ($request) { + {{querySearch}}; + }) + ->paginate() + ->withQueryString() + ]); + } + + /** + * Show the form for creating a new resource. + * + * @return \Inertia\Response + */ + public function create() + { + return Inertia::render('{{modelNamePluralUpperCase}}/Create'); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store({{modelName}}Request $request) + { + $validated = $request->validated(); + + {{modelName}}::create($validated); + + return Redirect::route('{{modelRoute}}.index')->with('success', '{{modelTitle}} created successfully.'); + } + + /** + * Display the specified resource. + * + * @param \{{modelNamespace}}\{{modelName}} ${{modelNameLowerCase}} + * @return \Inertia\Response + */ + public function show({{modelName}} ${{modelNameLowerCase}}) + { + return Inertia::render('{{modelNamePluralUpperCase}}/Show', [ + '{{modelNameLowerCase}}' => ${{modelNameLowerCase}} + ]); + } + + /** + * Show the form for editing the specified resource. + * + * @param \{{modelNamespace}}\{{modelName}} ${{modelNameLowerCase}} + * @return \Inertia\Response + */ + public function edit({{modelName}} ${{modelNameLowerCase}}) + { + return Inertia::render('{{modelNamePluralUpperCase}}/Edit', [ + '{{modelNameLowerCase}}' => ${{modelNameLowerCase}} + ]); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param \{{modelNamespace}}\{{modelName}} ${{modelNameLowerCase}} + * @return \Illuminate\Http\RedirectResponse + */ + public function update({{modelName}}Request $request, {{modelName}} ${{modelNameLowerCase}}) + { + $validated = $request->validated(); + + ${{modelNameLowerCase}}->update($validated); + + return Redirect::route('{{modelRoute}}.index')->with('success', '{{modelTitle}} updated successfully.'); + } + + /** + * Remove the specified resource from storage. + * + * @param \{{modelNamespace}}\{{modelName}} ${{modelNameLowerCase}} + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy({{modelName}} ${{modelNameLowerCase}}) + { + ${{modelNameLowerCase}}->delete(); + + return Redirect::route('{{modelRoute}}.index')->with('success', '{{modelTitle}} deleted successfully.'); + } +} \ No newline at end of file diff --git a/src/stubs/views/jetstream/Create.stub b/src/stubs/views/jetstream/Create.stub new file mode 100644 index 0000000..f6abe31 --- /dev/null +++ b/src/stubs/views/jetstream/Create.stub @@ -0,0 +1,59 @@ + + + \ No newline at end of file diff --git a/src/stubs/views/jetstream/Edit.stub b/src/stubs/views/jetstream/Edit.stub new file mode 100644 index 0000000..0a5aff2 --- /dev/null +++ b/src/stubs/views/jetstream/Edit.stub @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/src/stubs/views/jetstream/Index.stub b/src/stubs/views/jetstream/Index.stub new file mode 100644 index 0000000..70c365b --- /dev/null +++ b/src/stubs/views/jetstream/Index.stub @@ -0,0 +1,110 @@ + + + \ No newline at end of file diff --git a/src/stubs/views/jetstream/Show.stub b/src/stubs/views/jetstream/Show.stub new file mode 100644 index 0000000..5b3cc32 --- /dev/null +++ b/src/stubs/views/jetstream/Show.stub @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/src/stubs/views/jetstream/form-field.stub b/src/stubs/views/jetstream/form-field.stub new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/stubs/views/jetstream/form-field.stub @@ -0,0 +1 @@ + diff --git a/src/stubs/views/jetstream/form.stub b/src/stubs/views/jetstream/form.stub new file mode 100644 index 0000000..bef3f0b --- /dev/null +++ b/src/stubs/views/jetstream/form.stub @@ -0,0 +1,18 @@ +
+ + +
{{ form.errors.{{column}} }}
+
+ +
+

{{title}}:

+

{{ {{modelNameLowerCase}}.{{column}} }}

+
\ No newline at end of file diff --git a/src/stubs/views/jetstream/view-field.stub b/src/stubs/views/jetstream/view-field.stub new file mode 100644 index 0000000..e69de29