feat: implement module generator with dynamic datagrid components and schema migration support

This commit is contained in:
Ümit Tunç
2026-04-28 23:55:39 +03:00
parent 74901dc3c6
commit 90d0a2a041
6 changed files with 274 additions and 107 deletions
@@ -28,8 +28,6 @@ class ModuleGeneratorController extends Controller
$request->validate([ $request->validate([
'title' => 'required|string|max:255', 'title' => 'required|string|max:255',
'columns' => 'required|array', 'columns' => 'required|array',
'columns.*.name' => 'required|string',
'columns.*.type' => 'required|string',
]); ]);
$title = $request->title; $title = $request->title;
@@ -45,8 +43,10 @@ class ModuleGeneratorController extends Controller
$migrationContent = "<?php\n\nuse Illuminate\Database\Migrations\Migration;\nuse Illuminate\Database\Schema\Blueprint;\nuse Illuminate\Support\Facades\Schema;\n\nclass Create" . Str::studly($tableName) . "Table extends Migration\n{\n public function up()\n {\n Schema::create('{$tableName}', function (Blueprint \$table) {\n \$table->id();\n"; $migrationContent = "<?php\n\nuse Illuminate\Database\Migrations\Migration;\nuse Illuminate\Database\Schema\Blueprint;\nuse Illuminate\Support\Facades\Schema;\n\nclass Create" . Str::studly($tableName) . "Table extends Migration\n{\n public function up()\n {\n Schema::create('{$tableName}', function (Blueprint \$table) {\n \$table->id();\n";
foreach ($request->columns as $column) { foreach ($request->columns as $column) {
$colName = Str::snake($column['name']);
$uiType = $column['type']; $uiType = $column['type'];
if ($uiType === 'separator') continue; // No column for separator
$colName = Str::snake($column['name']);
$dbType = 'string'; $dbType = 'string';
switch ($uiType) { switch ($uiType) {
@@ -57,6 +57,7 @@ class ModuleGeneratorController extends Controller
case 'time': $dbType = 'time'; break; case 'time': $dbType = 'time'; break;
case 'long-text': $dbType = 'longText'; break; case 'long-text': $dbType = 'longText'; break;
case 'boolean': $dbType = 'boolean'; break; case 'boolean': $dbType = 'boolean'; break;
case 'multiple-choice': $dbType = 'text'; break; // Use text for multiple selection
default: $dbType = 'string'; break; default: $dbType = 'string'; break;
} }
@@ -99,9 +100,40 @@ class ModuleGeneratorController extends Controller
// Generate relationDatas // Generate relationDatas
$relationDatasCode = "[\n"; $relationDatasCode = "[\n";
foreach ($request->columns as $column) { foreach ($request->columns as $column) {
$uiType = $column['type'];
if ($uiType === 'separator') {
$label = $column['label'] ?? 'Separator';
$relationDatasCode .= " 'separator_" . rand(100, 999) . "' => [\n 'type' => 'separator',\n 'label' => '{$label}',\n ],\n";
continue;
}
$colName = Str::snake($column['name']); $colName = Str::snake($column['name']);
$colType = $column['type']; $relationDatasCode .= " '{$colName}' => [\n 'type' => '{$uiType}',\n";
$relationDatasCode .= " '{$colName}' => [\n 'type' => '{$colType}',\n ],\n";
if ($uiType === 'select' || $uiType === 'multiple-choice') {
$optionsStr = $column['options'] ?? '';
$optionsArr = array_map('trim', explode(',', $optionsStr));
$relationDatasCode .= " 'datas' => [\n";
foreach ($optionsArr as $opt) {
if (empty($opt)) continue;
$relationDatasCode .= " ['id' => '{$opt}', 'name' => '{$opt}'],\n";
}
$relationDatasCode .= " ],\n";
if ($uiType === 'select') {
$relationDatasCode .= " 'value' => 'id',\n";
}
if ($uiType === 'multiple-choice') {
$relationDatasCode .= " 'pattern' => '{name}',\n";
$relationDatasCode .= " 'seperator' => ',',\n";
}
} elseif ($uiType === 'select-dropdown') {
$source = $column['source'] ?? '';
$relationDatasCode .= " 'dataSource' => '{$source}',\n";
$relationDatasCode .= " 'datas' => [],\n";
$relationDatasCode .= " 'pattern' => '{name}',\n";
}
$relationDatasCode .= " ],\n";
} }
$relationDatasCode .= "]"; $relationDatasCode .= "]";
@@ -125,7 +157,7 @@ class ModuleGeneratorController extends Controller
$type->slug = $slug; $type->slug = $slug;
$type->icon = $iconName; $type->icon = $iconName;
$type->enabled = 1; $type->enabled = 1;
$type->full_control = "1"; // Default admin level $type->full_control = "1";
$type->write = "1"; $type->write = "1";
$type->read = "1"; $type->read = "1";
$type->modify = "1"; $type->modify = "1";
+203 -97
View File
@@ -1,91 +1,144 @@
@extends('admin.master') @extends('admin.master')
@section("title", e2("Module Generator")) @section("title", e2("Module Generator Wizard"))
@section("desc", e2("Create a new module with model, migration and view automatically.")) @section("desc", e2("Follow the steps to create a complex module."))
@section('content') @section('content')
<div class="content"> <div class="content">
<form action="{{ url('admin/module-generator/generate') }}" method="POST" enctype="multipart/form-data"> <div class="block" id="wizard-block">
@csrf <!-- Step Tabs -->
<div class="block"> <ul class="nav nav-tabs nav-tabs-alt nav-fill" role="tablist">
<div class="block-header block-header-default"> <li class="nav-item">
<h3 class="block-title">{{e2("Module Basic Information")}}</h3> <a class="nav-link active" id="step1-tab" data-toggle="tab" href="#step1" role="tab">{{e2("1. Basic Info")}}</a>
</div> </li>
<div class="block-content"> <li class="nav-item">
<div class="row"> <a class="nav-link disabled" id="step2-tab" data-toggle="tab" href="#step2" role="tab">{{e2("2. Database Columns")}}</a>
<div class="col-md-6"> </li>
<div class="form-group"> <li class="nav-item">
<label for="title">{{e2("Module Title")}}</label> <a class="nav-link disabled" id="step3-tab" data-toggle="tab" href="#step3" role="tab">{{e2("3. Permissions & Finish")}}</a>
<input type="text" name="title" id="title" class="form-control" placeholder="e.g. Products" required> </li>
</div> </ul>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">{{e2("Module Icon (PNG)")}}</label>
<input type="file" name="icon" id="icon" class="form-control" accept="image/png">
<small class="text-muted">Recommended size: 64x64px. Saved to assets/icons/</small>
</div>
</div>
</div>
</div>
</div>
<div class="block"> <form action="{{ url('admin/module-generator/generate') }}" method="POST" enctype="multipart/form-data" id="generator-form">
<div class="block-header block-header-default"> @csrf
<h3 class="block-title">{{e2("Database Columns")}}</h3> <div class="block-content tab-content" style="min-height: 400px;">
<div class="block-options"> <!-- Step 1: Basic Info -->
<button type="button" class="btn btn-sm btn-success" id="add-column"> <div class="tab-pane active" id="step1" role="tabpanel">
<i class="fa fa-plus mr-1"></i> {{e2("Add Column")}} <div class="row py-30 justify-content-center">
</button> <div class="col-sm-8 col-md-6">
<div class="form-group">
<label for="title">{{e2("Module Title")}}</label>
<input type="text" name="title" id="title" class="form-control form-control-lg" placeholder="e.g. Product Management" required>
</div>
<div class="form-group">
<label for="icon">{{e2("Module Icon (PNG)")}}</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="icon" name="icon" accept="image/png">
<label class="custom-file-label" for="icon">{{e2("Choose icon...")}}</label>
</div>
</div>
<div class="text-right">
<button type="button" class="btn btn-alt-primary next-step" data-next="step2">
{{e2("Next Step")}} <i class="fa fa-arrow-right ml-5"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Step 2: Columns -->
<div class="tab-pane" id="step2" role="tabpanel">
<div class="row justify-content-center">
<div class="col-12">
<div class="table-responsive">
<table class="table table-vcenter table-hover" id="columns-table">
<thead>
<tr>
<th>{{e2("Column Name")}}</th>
<th>{{e2("Type")}}</th>
<th>{{e2("Advanced Config")}}</th>
<th class="text-center" style="width: 50px;"></th>
</tr>
</thead>
<tbody id="columns-body">
<tr class="column-row" data-index="0">
<td>
<input type="text" name="columns[0][name]" class="form-control" placeholder="column_name" required>
</td>
<td>
<select name="columns[0][type]" class="form-control column-type-select">
<option value="string">String</option>
<option value="integer">Integer</option>
<option value="decimal">Decimal</option>
<option value="long-text">Long Text</option>
<option value="date">Date</option>
<option value="datetime">DateTime</option>
<option value="time">Time</option>
<option value="boolean">Boolean (Toggle)</option>
<option value="select">Static Dropdown</option>
<option value="select-dropdown">API Dropdown</option>
<option value="multiple-choice">Multiple Choice</option>
<option value="link-search">File / PDF Link</option>
<option value="separator">--- Separator ---</option>
</select>
</td>
<td>
<div class="config-container">
<small class="text-muted">{{e2("No extra config needed")}}</small>
</div>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-alt-danger remove-column">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="py-20">
<button type="button" class="btn btn-alt-success btn-block" id="add-column">
<i class="fa fa-plus mr-5"></i> {{e2("Add New Column")}}
</button>
</div>
<hr>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-alt-secondary prev-step" data-prev="step1">
<i class="fa fa-arrow-left mr-5"></i> {{e2("Back")}}
</button>
<button type="button" class="btn btn-alt-primary next-step" data-next="step3">
{{e2("Next Step")}} <i class="fa fa-arrow-right ml-5"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Step 3: Permissions & Finish -->
<div class="tab-pane" id="step3" role="tabpanel">
<div class="row py-30 justify-content-center">
<div class="col-sm-8 col-md-6 text-center">
<div class="alert alert-info">
<i class="fa fa-info-circle mr-5"></i>
{{e2("Default permissions will be assigned to Level 1 (Admin). You can adjust them later in System Settings.")}}
</div>
<h3 class="font-w400 mt-50">{{e2("Ready to generate?")}}</h3>
<p class="text-muted">{{e2("This will create a Migration, Model, and a Blade View.")}}</p>
<div class="py-30">
<button type="submit" class="btn btn-hero btn-lg btn-alt-primary">
<i class="fa fa-magic mr-10"></i> {{e2("Generate Module Now")}}
</button>
</div>
<button type="button" class="btn btn-alt-secondary prev-step" data-prev="step2">
<i class="fa fa-arrow-left mr-5"></i> {{e2("Back to Columns")}}
</button>
</div>
</div>
</div> </div>
</div> </div>
<div class="block-content"> </form>
<div class="table-responsive"> </div>
<table class="table table-bordered table-striped" id="columns-table">
<thead>
<tr>
<th>{{e2("Column Name")}}</th>
<th>{{e2("Type")}}</th>
<th class="text-center" style="width: 50px;">{{e2("Action")}}</th>
</tr>
</thead>
<tbody id="columns-body">
<tr>
<td>
<input type="text" name="columns[0][name]" class="form-control" placeholder="e.g. product_name" required>
</td>
<td>
<select name="columns[0][type]" class="form-control" required>
<option value="string">String (Text)</option>
<option value="integer">Integer (Number)</option>
<option value="decimal">Decimal (Price/Ratio)</option>
<option value="date">Date</option>
<option value="datetime">DateTime</option>
<option value="time">Time</option>
<option value="long-text">Long Text (Textarea)</option>
<option value="boolean">Boolean (Yes/No)</option>
<option value="select">Select (Static Dropdown)</option>
<option value="select-dropdown">Select (API Dropdown)</option>
<option value="link-search">Link Search (File/PDF)</option>
<option value="auto-complete">Auto Complete</option>
</select>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-danger remove-column">
<i class="fa fa-times"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="py-3">
<button type="submit" class="btn btn-primary btn-lg btn-block">
<i class="fa fa-magic mr-1"></i> {{e2("Generate Module")}}
</button>
</div>
</div>
</div>
</form>
</div> </div>
@section('scripts') @section('scripts')
@@ -93,31 +146,86 @@
$(function() { $(function() {
let colIndex = 1; let colIndex = 1;
// Step Navigation
$('.next-step').click(function() {
let nextTab = $(this).data('next');
$('#' + nextTab + '-tab').removeClass('disabled').tab('show');
});
$('.prev-step').click(function() {
let prevTab = $(this).data('prev');
$('#' + prevTab + '-tab').tab('show');
});
// Column Config Logic
function updateConfig(row) {
let type = row.find('.column-type-select').val();
let index = row.data('index');
let container = row.find('.config-container');
let html = '';
if (type === 'select' || type === 'multiple-choice') {
html = `
<div class="form-group mb-0">
<input type="text" name="columns[${index}][options]" class="form-control form-control-sm" placeholder="Option1, Option2, Option3" title="Comma separated options">
<small class="form-text text-muted">Comma separated values</small>
</div>
`;
} else if (type === 'select-dropdown') {
html = `
<div class="form-group mb-0">
<input type="text" name="columns[${index}][source]" class="form-control form-control-sm" placeholder="api/table_name or table_name">
<small class="form-text text-muted">Table name or API endpoint</small>
</div>
`;
} else if (type === 'separator') {
html = `
<div class="form-group mb-0">
<input type="text" name="columns[${index}][label]" class="form-control form-control-sm" placeholder="Separator Title">
</div>
`;
} else {
html = '<small class="text-muted">No extra config needed</small>';
}
container.html(html);
}
$(document).on('change', '.column-type-select', function() {
updateConfig($(this).closest('tr'));
});
$('#add-column').click(function() { $('#add-column').click(function() {
let html = ` let html = `
<tr> <tr class="column-row" data-index="${colIndex}">
<td> <td>
<input type="text" name="columns[${colIndex}][name]" class="form-control" placeholder="e.g. column_name" required> <input type="text" name="columns[${colIndex}][name]" class="form-control" placeholder="column_name" required>
</td> </td>
<td> <td>
<select name="columns[${colIndex}][type]" class="form-control" required> <select name="columns[${colIndex}][type]" class="form-control column-type-select">
<option value="string">String (Text)</option> <option value="string">String</option>
<option value="integer">Integer (Number)</option> <option value="integer">Integer</option>
<option value="decimal">Decimal (Price/Ratio)</option> <option value="decimal">Decimal</option>
<option value="long-text">Long Text</option>
<option value="date">Date</option> <option value="date">Date</option>
<option value="datetime">DateTime</option> <option value="datetime">DateTime</option>
<option value="time">Time</option> <option value="time">Time</option>
<option value="long-text">Long Text (Textarea)</option> <option value="boolean">Boolean (Toggle)</option>
<option value="boolean">Boolean (Yes/No)</option> <option value="select">Static Dropdown</option>
<option value="select">Select (Static Dropdown)</option> <option value="select-dropdown">API Dropdown</option>
<option value="select-dropdown">Select (API Dropdown)</option> <option value="multiple-choice">Multiple Choice</option>
<option value="link-search">Link Search (File/PDF)</option> <option value="link-search">File / PDF Link</option>
<option value="auto-complete">Auto Complete</option> <option value="separator">--- Separator ---</option>
</select> </select>
</td> </td>
<td>
<div class="config-container">
<small class="text-muted">No extra config needed</small>
</div>
</td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-sm btn-danger remove-column"> <button type="button" class="btn btn-sm btn-alt-danger remove-column">
<i class="fa fa-times"></i> <i class="fa fa-trash"></i>
</button> </button>
</td> </td>
</tr> </tr>
@@ -129,8 +237,6 @@
$(document).on('click', '.remove-column', function() { $(document).on('click', '.remove-column', function() {
if ($('#columns-body tr').length > 1) { if ($('#columns-body tr').length > 1) {
$(this).closest('tr').remove(); $(this).closest('tr').remove();
} else {
alert('At least one column is required.');
} }
}); });
}); });
@@ -0,0 +1,19 @@
{
dataField: "{{($column['name'])}}",
<?php
$captionName = isset($column['relation']['caption']) ? $column['relation']['caption'] : $column['name'];
$validationType = $column['relation']['validationType'] ?? 'custom';
?>
caption: "{{e2($captionName)}}",
dataType: "boolean",
validationRules: [{
type : '{{$validationType}}',
validationCallback: validationCallback_{{$column['name']}},
}],
setCellValue(rowData, value, data, options) {
@include("components.table.datagrid.js.init-cell-value")
rowData.{{$column['name']}} = value;
},
},
@@ -13,9 +13,10 @@
lookup: { lookup: {
dataSource : [ dataSource : [
<?php foreach($column['relation']['datas'] AS $data) { <?php foreach($column['relation']['datas'] AS $data) {
$key = $column['relation']['value']; $key = $column['relation']['value'] ?? 'id';
$val = is_array($data) ? ($data[$key] ?? '') : ($data->$key ?? '');
?> ?>
'{{$data->$key}}', '{{$val}}',
<?php <?php
} ?> } ?>
] ]
@@ -0,0 +1,8 @@
{
caption: "{{$column['label'] ?? '---'}}",
width: 20,
allowEditing: false,
allowFiltering: false,
allowSorting: false,
cssClass: "column-separator"
},
@@ -9,7 +9,8 @@
if(isset($matches[1])) { if(isset($matches[1])) {
foreach($matches[1] AS $match) { foreach($matches[1] AS $match) {
$patternString = str_replace("{" . $match . "}", $data->$match, $patternString); $value = is_array($data) ? ($data[$match] ?? '') : ($data->$match ?? '');
$patternString = str_replace("{" . $match . "}", $value, $patternString);
} }
} }
$multipleChoiceData[] = $patternString; $multipleChoiceData[] = $patternString;