Skip to content

Integrate Jetstream Vue crud generator #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d8fded5
update jetstram
Andoromain Apr 1, 2025
2a83803
update bug fix jetstream
Andoromain Apr 1, 2025
cb8fa8e
update bug fix jetstream
Andoromain Apr 1, 2025
bea54d8
update bug fix jetstream
Andoromain Apr 1, 2025
8119b97
update bug fix jetstream
Andoromain Apr 1, 2025
f93624d
update bug fix jetstream
Andoromain Apr 1, 2025
76efe9b
update bug fix jetstream
Andoromain Apr 1, 2025
c572287
update jetstram
Andoromain Apr 3, 2025
ad93c74
update jetstram
Andoromain Apr 3, 2025
3ffeab3
update bug fix
Andoromain Apr 3, 2025
847305a
update clean code
Andoromain Apr 3, 2025
8c00604
Merge pull request #1 from Andoromain/test
Andoromain Apr 3, 2025
e96f9ce
Merge branch 'awais-vteams:master' into master
Andoromain Apr 4, 2025
e3722e7
update composer
Andoromain Apr 6, 2025
f47c954
Merge branch 'master' of https://github.com/Andoromain/laravel-crud-g…
Andoromain Apr 6, 2025
3b7f100
update package name
Andoromain Apr 6, 2025
94ed855
update composer
Andoromain Apr 6, 2025
f16493a
update composer
Andoromain Apr 6, 2025
4ace798
update composer
Andoromain Apr 6, 2025
4e8b215
update composer
Andoromain Apr 6, 2025
d1dad05
debug
Andoromain Apr 6, 2025
58e1eb8
debug
Andoromain Apr 6, 2025
7fd0f47
debug
Andoromain Apr 6, 2025
15110e1
debug
Andoromain Apr 6, 2025
8275ae1
debug
Andoromain Apr 6, 2025
d884508
debug
Andoromain Apr 6, 2025
c3f54ae
debug
Andoromain Apr 6, 2025
376f4a9
debug
Andoromain Apr 6, 2025
4a36c21
update input type
Andoromain Apr 8, 2025
55f55ce
update input type
Andoromain Apr 8, 2025
e069965
update input type
Andoromain Apr 8, 2025
866f0e4
update input type
Andoromain Apr 8, 2025
179989e
update input type
Andoromain Apr 8, 2025
5c16a3b
update input type
Andoromain Apr 8, 2025
34d8abc
update input type
Andoromain Apr 8, 2025
7f46f55
update input type
Andoromain Apr 8, 2025
580f561
update
Andoromain Apr 8, 2025
8f25e22
update bug fix
Andoromain Apr 10, 2025
2061ca2
update bug fix
Andoromain Apr 10, 2025
6333a3f
update
Andoromain Apr 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

310 changes: 305 additions & 5 deletions src/Commands/CrudGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
];
}
Expand All @@ -89,6 +90,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp
'livewire' => 'livewire',
'react' => 'react',
'vue' => 'vue',
'jetstream' => 'jetstream',
default => 'bootstrap',
};
}
Expand All @@ -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);",
]
Expand All @@ -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();

Expand Down Expand Up @@ -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 .= <<<HTML
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
$title
</th>

HTML;

$tableBody .= <<<HTML
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ {$capitalizeModelName}.$column }}
</td>

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
<template>
<div v-if="links.length > 3">
<div class="flex flex-wrap -mb-1">
<template v-for="(link, key) in links" :key="key">
<div
v-if="link.url === null"
class="mr-1 mb-1 px-4 py-2 text-sm text-gray-500 border rounded"
:class="{ 'opacity-50': link.url === null }"
v-html="link.label"
/>
<Link
v-else
class="mr-1 mb-1 px-4 py-2 text-sm border rounded hover:bg-indigo-100"
:class="{
'bg-indigo-500 text-white': link.active,
'text-gray-700': !link.active
}"
:href="link.url"
v-html="link.label"
/>
</template>
</div>
</div>
</template>

<script>
import { Link } from '@inertiajs/vue3'
export default {
components: {
Link
},
props: {
links: Array
}
}
</script>
VUE;
}

protected function getSearchFilterComponent(): string
{
return <<<VUE
<template>
<div class="relative flex items-center">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<input
type="text"
class="py-2 pl-10 pr-4 w-full text-sm text-gray-700 placeholder-gray-400 bg-white border border-gray-300 rounded focus:outline-none focus:border-indigo-500"
placeholder="Search..."
:value="modelValue"
@input="updateValue"
/>
</div>
</template>

<script>
export default {
props: {
modelValue: String
},
emits: ['update:modelValue'],
data() {
return {
timeout: null
}
},
methods: {
updateValue(e) {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.\$emit('update:modelValue', e.target.value);
}, 300); // 300ms debounce
}
}
}
</script>
VUE;
}
/**
* @return $this
* @throws FileNotFoundException
Expand Down Expand Up @@ -258,25 +487,47 @@ 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(), [
'{{tableHeader}}' => $tableHead,
'{{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",
Expand All @@ -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 <<<HTML
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="$column">
$title
</label>
<input
id="$column"
v-model="form.$column"
type="$inputType"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
:class="{ 'border-red-500': form.errors.$column }"
>
<div v-if="form.errors.$column" class="text-red-500 text-xs italic">{{ form.errors.$column }}</div>
</div>
HTML;
}

protected function getJetstreamDetailField(string $title, string $column): string
{
$capitalizeModelName = lcfirst($this->name);
return <<<HTML
<div class="mb-4">
<h3 class="text-gray-700 font-bold">$title:</h3>
<p class="text-gray-600">{{ {$capitalizeModelName}.$column }}</p>
</div>

HTML;
}
}
Loading