D
DevlogConstruyendo en público
Volver
nexuscmsmodule-manageradminlaravel

NexusCMS: Module Manager y su nuevo Admin

Como funciona el nuevo Admin del Module Manager en NexusCMS y por que acelera el desarrollo de modulos

Team
5 min de lectura

En NexusCMS acabamos de consolidar una mejora clave en el Module Manager: ahora los modulos pueden integrarse al panel de administracion de dos formas claras y predecibles.

Que hay de nuevo en el Admin

La novedad importante es que el Module Manager entiende el campo admin_crud dentro de cada module.json y activa el flujo adecuado:

  1. json: CRUD administrativo generico basado en configuracion
  2. custom: rutas y vistas administrativas propias del modulo

Esto nos da un equilibrio muy util: velocidad para casos comunes y flexibilidad total para casos complejos.

Modo 1: Admin rapido con json

Cuando un modulo define admin_crud: "json", todo el flujo CRUD se auto-registra sin escribir una sola ruta. Veamos como funciona paso a paso.

Paso 1: Configuracion en module.json

Defines la config JSON y el menu en un unico lugar:

{
  "name": "Store",
  "enabled": true,
  "routes": true,
  "migrations": true,
  "views": true,
  "admin_crud": "json",
  "admin_menu": {
    "label": "Store",
    "icon": "fas fa-shopping-cart",
    "route": "admin.store.crud.index",
    "order": 25
  }
}

El campo admin_crud: "json" es la clave que dispara todo el mecanismo.

Paso 2: ModuleServiceProvider auto-registra las rutas

En el boot de ModuleServiceProvider, el sistema detecta todos los modulos con admin_crud: "json" e inyecta 6 rutas automaticamente:

// Genera esto internamente:
Route::middleware(['web', 'auth', 'permission:access.admin.panel'])
    ->prefix('acp/store/manage')
    ->name('admin.store.crud.')
    ->group(function () {
        // GET /acp/store/manage
        Route::get('/', [GenericCrudController::class, 'index'])->name('index');
        
        // GET /acp/store/manage/create
        Route::get('/create', [GenericCrudController::class, 'create'])->name('create');
        
        // POST /acp/store/manage (guardar nuevo)
        Route::post('/', [GenericCrudController::class, 'store'])->name('store');
        
        // GET /acp/store/manage/{id}/edit
        Route::get('/{id}/edit', [GenericCrudController::class, 'edit'])->name('edit');
        
        // PUT /acp/store/manage/{id} (actualizar)
        Route::put('/{id}', [GenericCrudController::class, 'update'])->name('update');
        
        // DELETE /acp/store/manage/{id}
        Route::delete('/{id}', [GenericCrudController::class, 'destroy'])->name('destroy');
    });

Sin tocar el modulo, ya tienes CRUD completo protegido por auth y permisos.

Paso 3: GenericCrudController interpreta la config

Cada accion del controller (index, create, store, etc) lee la config del modulo y adapta el comportamiento:

class GenericCrudController extends Controller
{
    public function index(Request $request, string $module)
    {
        // Obtiene la config del modulo
        $config = ModuleRegistryService::getModuleConfig($module);
        
        // Valida que tenga lista y columnas definidas
        $listConfig = $config['list'] ?? [];
        
        // Ejecuta busquedas, paginacion, etc segun la config
        $records = $this->buildQuery($config)
            ->paginate(15);
        
        // Retorna la vista generica con datos
        return view('admin::generic-crud.index', [
            'config' => $config,
            'records' => $records,
            'base' => "admin.{$module}.crud.",
        ]);
    }
}

El controller NO sabe de Store especifico, solo interpreta la configuracion dinamicamente.

Paso 4: Vista generica renderiza tabla + formularios

La misma vista admin::generic-crud.index se usa para todos los modulos JSON. Renderiza:

  • Encabezado con titulo de la config
  • Campo de busqueda si searchable: true
  • Tabla con columnas segun list.columns
  • Botones de editar/eliminar con rutas dinamicas
  • Paginacion
<table>
    <thead>
        @foreach($config['list']['columns'] as $col)
            <th>{{ $col['label'] ?? $col['field'] }}</th>
        @endforeach
    </thead>
    <tbody>
        @foreach($records as $record)
            <tr>
                @foreach($config['list']['columns'] as $col)
                    <td>{{ $record->{$col['field']} }}</td>
                @endforeach
                <td>
                    <a href="{{ route($base.'edit', $record->id) }}">Edit</a>
                    <form action="{{ route($base.'destroy', $record->id) }}" method="POST">
                        <button type="submit">Delete</button>
                    </form>
                </td>
            </tr>
        @endforeach
    </tbody>
</table>

Todo esta parametrizado. Una sola vista sirve para Store, Products, Categories, etc.

Ejemplo real: Store Products en NexusCMS

En el modulo Store de NexusCMS, el archivo admin-crud.json define todo el CRUD para productos:

{
    "title": "Store Products",
    "model": "Modules\\Store\\Domain\\Models\\StoreProduct",
    "list": {
        "searchable": true,
        "search_columns": ["name", "key", "type"],
        "order_by": "sort_order",
        "order_dir": "asc",
        "columns": [
            { "field": "sort_order", "label": "#" },
            { "field": "name",       "label": "Product" },
            { "field": "key",        "label": "Key" },
            { "field": "cost",       "label": "Cost (DP)" },
            { "field": "active",     "label": "Active", "format": "boolean" }
        ]
    },
    "form": {
        "sections": [
            {
                "title": "Product Info",
                "fields": [
                    { "name": "name", "label": "Name", "type": "text", "required": true },
                    { "name": "key", "label": "Key", "type": "text", "required": true },
                    { "name": "description", "label": "Description", "type": "textarea" }
                ]
            },
            {
                "title": "Pricing & Settings",
                "fields": [
                    { "name": "cost", "label": "Cost (DP)", "type": "number", "required": true },
                    { "name": "active", "label": "Active", "type": "checkbox" }
                ]
            }
        ]
    }
}

Mira lo que sucede:

En la lista:

  • GenericCrudController lee list.columns y renderiza exactamente esas 5 columnas
  • El campo search_columns habilita busqueda en name, key y type
  • order_by: "sort_order" ordena por numero de posicion
  • El formato "boolean" renderiza checkboxes

En el formulario:

  • Se crean 2 secciones (Product Info, Pricing & Settings)
  • Cada seccion agrupa campos relacionados
  • El controller valida required: true antes de guardar
  • Los tipos (text, textarea, number, checkbox) determinan el input HTML

Sin escribir rutas, controller ni vistas.

Solo este JSON genera:

  • Listado con busqueda y ordenamiento
  • Formulario en 2 secciones
  • Validacion de campos obligatorios
  • Guardado y actualizacion de datos

Todo parametrizado, nada hardcodeado.

Por que esto es potente

  • Zero boilerplate - Una config JSON reemplaza 200+ lineas de codigo
  • Consistente - Todos los modulos JSON usan la misma UI
  • Escalable - Si cambias la vista generica, todos se benefician
  • Flexible - El custom mode existe para casos que necesiten mas control

Modo 2: Admin a medida con custom

Si el modulo necesita una UX especifica o logica mas avanzada, usamos admin_crud: "custom".

{
  "name": "Donate",
  "admin_crud": "custom",
  "admin_menu": {
    "label": "Donations",
    "route": "admin.donate.plans.index"
  }
}

En este modo, el provider base del modulo carga automaticamente:

  • Admin/routes.php
  • Admin/views

Asi mantenemos el mismo punto de entrada del Module Manager, pero con total control sobre el panel.

Por que esto mejora el desarrollo

Lo mejor de este cambio es que define un contrato simple para todos los modulos:

  • Si quieres ir rapido: json
  • Si necesitas algo avanzado: custom

No hay que pelear con convenciones ambiguas. El equipo sabe exactamente como conectar cada modulo al Admin y escalarlo despues sin rehacer todo.

Lo que viene

Nuestro siguiente paso es seguir estandarizando configuraciones para que mas modulos puedan arrancar en modo json desde el dia uno, y saltar a custom solo cuando de verdad haga falta.


La evolucion del Module Manager va en esa direccion: menos friccion, mas consistencia, mejor Admin.