feat: implement module generator with dynamic datagrid components and schema migration support
This commit is contained in:
@@ -28,8 +28,6 @@ class ModuleGeneratorController extends Controller
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'columns' => 'required|array',
|
||||
'columns.*.name' => 'required|string',
|
||||
'columns.*.type' => 'required|string',
|
||||
]);
|
||||
|
||||
$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";
|
||||
|
||||
foreach ($request->columns as $column) {
|
||||
$colName = Str::snake($column['name']);
|
||||
$uiType = $column['type'];
|
||||
if ($uiType === 'separator') continue; // No column for separator
|
||||
|
||||
$colName = Str::snake($column['name']);
|
||||
|
||||
$dbType = 'string';
|
||||
switch ($uiType) {
|
||||
@@ -57,6 +57,7 @@ class ModuleGeneratorController extends Controller
|
||||
case 'time': $dbType = 'time'; break;
|
||||
case 'long-text': $dbType = 'longText'; break;
|
||||
case 'boolean': $dbType = 'boolean'; break;
|
||||
case 'multiple-choice': $dbType = 'text'; break; // Use text for multiple selection
|
||||
default: $dbType = 'string'; break;
|
||||
}
|
||||
|
||||
@@ -99,9 +100,40 @@ class ModuleGeneratorController extends Controller
|
||||
// Generate relationDatas
|
||||
$relationDatasCode = "[\n";
|
||||
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']);
|
||||
$colType = $column['type'];
|
||||
$relationDatasCode .= " '{$colName}' => [\n 'type' => '{$colType}',\n ],\n";
|
||||
$relationDatasCode .= " '{$colName}' => [\n 'type' => '{$uiType}',\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 .= "]";
|
||||
|
||||
@@ -125,7 +157,7 @@ class ModuleGeneratorController extends Controller
|
||||
$type->slug = $slug;
|
||||
$type->icon = $iconName;
|
||||
$type->enabled = 1;
|
||||
$type->full_control = "1"; // Default admin level
|
||||
$type->full_control = "1";
|
||||
$type->write = "1";
|
||||
$type->read = "1";
|
||||
$type->modify = "1";
|
||||
|
||||
@@ -1,123 +1,231 @@
|
||||
@extends('admin.master')
|
||||
@section("title", e2("Module Generator"))
|
||||
@section("desc", e2("Create a new module with model, migration and view automatically."))
|
||||
@section("title", e2("Module Generator Wizard"))
|
||||
@section("desc", e2("Follow the steps to create a complex module."))
|
||||
@section('content')
|
||||
|
||||
<div class="content">
|
||||
<form action="{{ url('admin/module-generator/generate') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="block">
|
||||
<div class="block-header block-header-default">
|
||||
<h3 class="block-title">{{e2("Module Basic Information")}}</h3>
|
||||
</div>
|
||||
<div class="block-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="title">{{e2("Module Title")}}</label>
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="e.g. Products" required>
|
||||
</div>
|
||||
</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" id="wizard-block">
|
||||
<!-- Step Tabs -->
|
||||
<ul class="nav nav-tabs nav-tabs-alt nav-fill" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="step1-tab" data-toggle="tab" href="#step1" role="tab">{{e2("1. Basic Info")}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" id="step2-tab" data-toggle="tab" href="#step2" role="tab">{{e2("2. Database Columns")}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" id="step3-tab" data-toggle="tab" href="#step3" role="tab">{{e2("3. Permissions & Finish")}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="block">
|
||||
<div class="block-header block-header-default">
|
||||
<h3 class="block-title">{{e2("Database Columns")}}</h3>
|
||||
<div class="block-options">
|
||||
<button type="button" class="btn btn-sm btn-success" id="add-column">
|
||||
<i class="fa fa-plus mr-1"></i> {{e2("Add Column")}}
|
||||
</button>
|
||||
<form action="{{ url('admin/module-generator/generate') }}" method="POST" enctype="multipart/form-data" id="generator-form">
|
||||
@csrf
|
||||
<div class="block-content tab-content" style="min-height: 400px;">
|
||||
<!-- Step 1: Basic Info -->
|
||||
<div class="tab-pane active" id="step1" role="tabpanel">
|
||||
<div class="row py-30 justify-content-center">
|
||||
<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 class="block-content">
|
||||
<div class="table-responsive">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
$(function() {
|
||||
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() {
|
||||
let html = `
|
||||
<tr>
|
||||
<tr class="column-row" data-index="${colIndex}">
|
||||
<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>
|
||||
<select name="columns[${colIndex}][type]" class="form-control" required>
|
||||
<option value="string">String (Text)</option>
|
||||
<option value="integer">Integer (Number)</option>
|
||||
<option value="decimal">Decimal (Price/Ratio)</option>
|
||||
<select name="columns[${colIndex}][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="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>
|
||||
<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">No extra config needed</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-danger remove-column">
|
||||
<i class="fa fa-times"></i>
|
||||
<button type="button" class="btn btn-sm btn-alt-danger remove-column">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -129,8 +237,6 @@
|
||||
$(document).on('click', '.remove-column', function() {
|
||||
if ($('#columns-body tr').length > 1) {
|
||||
$(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: {
|
||||
dataSource : [
|
||||
<?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
|
||||
} ?>
|
||||
]
|
||||
|
||||
@@ -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])) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user