fix(admin): corregir guardado customcssmanager con toast
- crear bootstrap para handler post en admin_init - ocultar botones globales para custom-css-manager - simplificar formbuilder eliminando handler duplicado - reemplazar alert por toast para notificaciones 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Bootstrap;
|
||||||
|
|
||||||
|
use ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence\WordPressSnippetRepository;
|
||||||
|
use ROITheme\Admin\CustomCSSManager\Application\UseCases\SaveSnippetUseCase;
|
||||||
|
use ROITheme\Admin\CustomCSSManager\Application\UseCases\DeleteSnippetUseCase;
|
||||||
|
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||||
|
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||||
|
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap para CustomCSSManager
|
||||||
|
*
|
||||||
|
* Registra el handler de formulario POST en admin_init
|
||||||
|
* ANTES de que se envíen headers HTTP
|
||||||
|
*/
|
||||||
|
final class CustomCSSManagerBootstrap
|
||||||
|
{
|
||||||
|
private const NONCE_ACTION = 'roi_custom_css_manager';
|
||||||
|
|
||||||
|
public static function init(): void
|
||||||
|
{
|
||||||
|
add_action('admin_init', [self::class, 'handleFormSubmission']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function handleFormSubmission(): void
|
||||||
|
{
|
||||||
|
if (!isset($_POST['roi_css_action'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que estamos en la página correcta
|
||||||
|
$page = $_GET['page'] ?? '';
|
||||||
|
$component = $_GET['component'] ?? '';
|
||||||
|
if ($page !== 'roi-theme-admin' || $component !== 'custom-css-manager') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar nonce
|
||||||
|
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', self::NONCE_ACTION)) {
|
||||||
|
wp_die('Nonce verification failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permisos
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$repository = new WordPressSnippetRepository($wpdb);
|
||||||
|
$saveUseCase = new SaveSnippetUseCase($repository);
|
||||||
|
$deleteUseCase = new DeleteSnippetUseCase($repository);
|
||||||
|
|
||||||
|
$action = sanitize_text_field($_POST['roi_css_action']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
match ($action) {
|
||||||
|
'save' => self::processSave($_POST, $saveUseCase),
|
||||||
|
'delete' => self::processDelete($_POST, $deleteUseCase),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Redirect con mensaje de éxito
|
||||||
|
$redirect_url = admin_url('admin.php?page=roi-theme-admin&component=custom-css-manager&roi_message=success');
|
||||||
|
wp_redirect($redirect_url);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
$redirect_url = admin_url('admin.php?page=roi-theme-admin&component=custom-css-manager&roi_message=error&roi_error=' . urlencode($e->getMessage()));
|
||||||
|
wp_redirect($redirect_url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function processSave(array $data, SaveSnippetUseCase $useCase): void
|
||||||
|
{
|
||||||
|
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||||
|
if (empty($id)) {
|
||||||
|
$id = SnippetId::generate()->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = SaveSnippetRequest::fromArray([
|
||||||
|
'id' => $id,
|
||||||
|
'name' => sanitize_text_field($data['snippet_name'] ?? ''),
|
||||||
|
'description' => sanitize_textarea_field($data['snippet_description'] ?? ''),
|
||||||
|
'css' => wp_strip_all_tags($data['snippet_css'] ?? ''),
|
||||||
|
'type' => sanitize_text_field($data['snippet_type'] ?? 'deferred'),
|
||||||
|
'pages' => array_map('sanitize_text_field', $data['snippet_pages'] ?? ['all']),
|
||||||
|
'enabled' => isset($data['snippet_enabled']),
|
||||||
|
'order' => absint($data['snippet_order'] ?? 100),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$useCase->execute($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function processDelete(array $data, DeleteSnippetUseCase $useCase): void
|
||||||
|
{
|
||||||
|
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||||
|
if (empty($id)) {
|
||||||
|
throw new ValidationException('ID de snippet requerido para eliminar');
|
||||||
|
}
|
||||||
|
$useCase->execute($id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,7 @@ namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Ui;
|
|||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
use ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence\WordPressSnippetRepository;
|
use ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence\WordPressSnippetRepository;
|
||||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\SaveSnippetUseCase;
|
|
||||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\DeleteSnippetUseCase;
|
|
||||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\GetAllSnippetsUseCase;
|
use ROITheme\Admin\CustomCSSManager\Application\UseCases\GetAllSnippetsUseCase;
|
||||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
|
||||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
|
||||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para gestión de CSS snippets en Admin Panel
|
* FormBuilder para gestión de CSS snippets en Admin Panel
|
||||||
@@ -19,6 +14,9 @@ use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
|||||||
* - Constructor recibe AdminDashboardRenderer
|
* - Constructor recibe AdminDashboardRenderer
|
||||||
* - Método buildForm() genera el HTML del formulario
|
* - Método buildForm() genera el HTML del formulario
|
||||||
*
|
*
|
||||||
|
* NOTA: El handler de formulario POST está en CustomCSSManagerBootstrap
|
||||||
|
* para que se ejecute en admin_init ANTES de que se envíen headers HTTP.
|
||||||
|
*
|
||||||
* Design System: Gradiente navy #0E2337 → #1e3a5f, accent #FF8600
|
* Design System: Gradiente navy #0E2337 → #1e3a5f, accent #FF8600
|
||||||
*/
|
*/
|
||||||
final class CustomCSSManagerFormBuilder
|
final class CustomCSSManagerFormBuilder
|
||||||
@@ -28,120 +26,15 @@ final class CustomCSSManagerFormBuilder
|
|||||||
|
|
||||||
private WordPressSnippetRepository $repository;
|
private WordPressSnippetRepository $repository;
|
||||||
private GetAllSnippetsUseCase $getAllUseCase;
|
private GetAllSnippetsUseCase $getAllUseCase;
|
||||||
private SaveSnippetUseCase $saveUseCase;
|
|
||||||
private DeleteSnippetUseCase $deleteUseCase;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AdminDashboardRenderer $renderer
|
private readonly AdminDashboardRenderer $renderer
|
||||||
) {
|
) {
|
||||||
// Crear repositorio y Use Cases internamente
|
// Crear repositorio y Use Case para listar snippets
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$this->repository = new WordPressSnippetRepository($wpdb);
|
$this->repository = new WordPressSnippetRepository($wpdb);
|
||||||
$this->getAllUseCase = new GetAllSnippetsUseCase($this->repository);
|
$this->getAllUseCase = new GetAllSnippetsUseCase($this->repository);
|
||||||
$this->saveUseCase = new SaveSnippetUseCase($this->repository);
|
// NOTA: El handler POST está en CustomCSSManagerBootstrap (admin_init)
|
||||||
$this->deleteUseCase = new DeleteSnippetUseCase($this->repository);
|
|
||||||
|
|
||||||
// Registrar handler de formulario POST
|
|
||||||
$this->registerFormHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registra handler para procesar formularios POST
|
|
||||||
*/
|
|
||||||
private function registerFormHandler(): void
|
|
||||||
{
|
|
||||||
// Solo registrar una vez
|
|
||||||
static $registered = false;
|
|
||||||
if ($registered) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$registered = true;
|
|
||||||
|
|
||||||
add_action('admin_init', function() {
|
|
||||||
$this->handleFormSubmission();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Procesa envío de formulario
|
|
||||||
*/
|
|
||||||
public function handleFormSubmission(): void
|
|
||||||
{
|
|
||||||
if (!isset($_POST['roi_css_action'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar nonce
|
|
||||||
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', self::NONCE_ACTION)) {
|
|
||||||
wp_die('Nonce verification failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar permisos
|
|
||||||
if (!current_user_can('manage_options')) {
|
|
||||||
wp_die('Insufficient permissions');
|
|
||||||
}
|
|
||||||
|
|
||||||
$action = sanitize_text_field($_POST['roi_css_action']);
|
|
||||||
|
|
||||||
try {
|
|
||||||
match ($action) {
|
|
||||||
'save' => $this->processSave($_POST),
|
|
||||||
'delete' => $this->processDelete($_POST),
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Redirect con mensaje de éxito
|
|
||||||
wp_redirect(add_query_arg('roi_message', 'success', wp_get_referer()));
|
|
||||||
exit;
|
|
||||||
|
|
||||||
} catch (ValidationException $e) {
|
|
||||||
// Redirect con mensaje de error
|
|
||||||
wp_redirect(add_query_arg([
|
|
||||||
'roi_message' => 'error',
|
|
||||||
'roi_error' => urlencode($e->getMessage())
|
|
||||||
], wp_get_referer()));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Procesa guardado de snippet
|
|
||||||
*/
|
|
||||||
private function processSave(array $data): void
|
|
||||||
{
|
|
||||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
|
||||||
|
|
||||||
// Generar ID si es nuevo
|
|
||||||
if (empty($id)) {
|
|
||||||
$id = SnippetId::generate()->value();
|
|
||||||
}
|
|
||||||
|
|
||||||
$request = SaveSnippetRequest::fromArray([
|
|
||||||
'id' => $id,
|
|
||||||
'name' => sanitize_text_field($data['snippet_name'] ?? ''),
|
|
||||||
'description' => sanitize_textarea_field($data['snippet_description'] ?? ''),
|
|
||||||
'css' => wp_strip_all_tags($data['snippet_css'] ?? ''),
|
|
||||||
'type' => sanitize_text_field($data['snippet_type'] ?? 'deferred'),
|
|
||||||
'pages' => array_map('sanitize_text_field', $data['snippet_pages'] ?? ['all']),
|
|
||||||
'enabled' => isset($data['snippet_enabled']),
|
|
||||||
'order' => absint($data['snippet_order'] ?? 100),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->saveUseCase->execute($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Procesa eliminación de snippet
|
|
||||||
*/
|
|
||||||
private function processDelete(array $data): void
|
|
||||||
{
|
|
||||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
|
||||||
|
|
||||||
if (empty($id)) {
|
|
||||||
throw new ValidationException('ID de snippet requerido para eliminar');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->deleteUseCase->execute($id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,13 +53,9 @@ final class CustomCSSManagerFormBuilder
|
|||||||
// Header
|
// Header
|
||||||
$html .= $this->buildHeader($componentId, count($snippets));
|
$html .= $this->buildHeader($componentId, count($snippets));
|
||||||
|
|
||||||
// Mensajes flash
|
// Toast para mensajes (usa el sistema existente de admin-dashboard.js)
|
||||||
if ($message) {
|
if ($message) {
|
||||||
$html .= sprintf(
|
$html .= $this->buildToastTrigger($message);
|
||||||
'<div class="alert alert-%s m-3">%s</div>',
|
|
||||||
esc_attr($message['type']),
|
|
||||||
esc_html($message['text'])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lista de snippets existentes
|
// Lista de snippets existentes
|
||||||
@@ -367,7 +256,7 @@ final class CustomCSSManagerFormBuilder
|
|||||||
// Botones
|
// Botones
|
||||||
$html .= ' <div class="col-12">';
|
$html .= ' <div class="col-12">';
|
||||||
$html .= ' <button type="submit" class="btn text-white" style="background-color: #FF8600;">';
|
$html .= ' <button type="submit" class="btn text-white" style="background-color: #FF8600;">';
|
||||||
$html .= ' <i class="bi bi-save me-1"></i> Guardar Snippet';
|
$html .= ' <i class="bi bi-check-circle me-1"></i> Guardar Cambios';
|
||||||
$html .= ' </button>';
|
$html .= ' </button>';
|
||||||
$html .= ' <button type="button" class="btn btn-secondary" onclick="resetCssForm()">';
|
$html .= ' <button type="button" class="btn btn-secondary" onclick="resetCssForm()">';
|
||||||
$html .= ' <i class="bi bi-x-circle me-1"></i> Cancelar';
|
$html .= ' <i class="bi bi-x-circle me-1"></i> Cancelar';
|
||||||
@@ -449,14 +338,84 @@ final class CustomCSSManagerFormBuilder
|
|||||||
$message = $_GET['roi_message'] ?? null;
|
$message = $_GET['roi_message'] ?? null;
|
||||||
|
|
||||||
if ($message === 'success') {
|
if ($message === 'success') {
|
||||||
return ['type' => 'success', 'text' => 'Snippet guardado correctamente'];
|
return ['type' => 'success', 'text' => 'Cambios guardados correctamente'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($message === 'error') {
|
if ($message === 'error') {
|
||||||
$error = urldecode($_GET['roi_error'] ?? 'Error desconocido');
|
$error = urldecode($_GET['roi_error'] ?? 'Error desconocido');
|
||||||
return ['type' => 'danger', 'text' => $error];
|
return ['type' => 'error', 'text' => $error];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genera script para mostrar Toast
|
||||||
|
*/
|
||||||
|
private function buildToastTrigger(array $message): string
|
||||||
|
{
|
||||||
|
$type = esc_js($message['type']);
|
||||||
|
$text = esc_js($message['text']);
|
||||||
|
|
||||||
|
// Mapear tipo a configuración de Bootstrap
|
||||||
|
$typeMap = [
|
||||||
|
'success' => ['bg' => 'success', 'icon' => 'bi-check-circle-fill'],
|
||||||
|
'error' => ['bg' => 'danger', 'icon' => 'bi-x-circle-fill'],
|
||||||
|
];
|
||||||
|
$config = $typeMap[$type] ?? $typeMap['success'];
|
||||||
|
$bg = $config['bg'];
|
||||||
|
$icon = $config['icon'];
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Crear container de toasts si no existe
|
||||||
|
let toastContainer = document.getElementById('roiToastContainer');
|
||||||
|
if (!toastContainer) {
|
||||||
|
toastContainer = document.createElement('div');
|
||||||
|
toastContainer.id = 'roiToastContainer';
|
||||||
|
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
|
||||||
|
toastContainer.style.top = '60px';
|
||||||
|
toastContainer.style.zIndex = '999999';
|
||||||
|
document.body.appendChild(toastContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear toast
|
||||||
|
const toastId = 'toast-' + Date.now();
|
||||||
|
const toastHTML = `
|
||||||
|
<div id="\${toastId}" class="toast align-items-center text-white bg-{$bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
<i class="bi {$icon} me-2"></i>
|
||||||
|
<strong>{$text}</strong>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||||
|
|
||||||
|
// Mostrar toast
|
||||||
|
const toastElement = document.getElementById(toastId);
|
||||||
|
const toast = new bootstrap.Toast(toastElement, {
|
||||||
|
autohide: true,
|
||||||
|
delay: 5000
|
||||||
|
});
|
||||||
|
toast.show();
|
||||||
|
|
||||||
|
// Eliminar del DOM después de ocultarse
|
||||||
|
toastElement.addEventListener('hidden.bs.toast', function() {
|
||||||
|
toastElement.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limpiar parámetros de URL sin recargar
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete('roi_message');
|
||||||
|
url.searchParams.delete('roi_error');
|
||||||
|
window.history.replaceState({}, '', url.toString());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ $group = $groupId && isset($groups[$groupId]) ? $groups[$groupId] : null;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Componentes con sistema de guardado propio (CRUD de entidades)
|
||||||
|
$componentsWithOwnSaveSystem = ['custom-css-manager'];
|
||||||
|
|
||||||
|
if (!in_array($activeComponent, $componentsWithOwnSaveSystem, true)):
|
||||||
|
?>
|
||||||
<!-- Botones Globales Save/Cancel -->
|
<!-- Botones Globales Save/Cancel -->
|
||||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
||||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||||
@@ -71,4 +77,5 @@ $group = $groupId && isset($groups[$groupId]) ? $groups[$groupId] : null;
|
|||||||
<?php echo esc_html__('Guardar Cambios', 'roi-theme'); ?>
|
<?php echo esc_html__('Guardar Cambios', 'roi-theme'); ?>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -329,6 +329,21 @@ if (!is_admin()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 5.2.1. CUSTOM CSS MANAGER BOOTSTRAP (Handler de formulario POST)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializar Bootstrap de CustomCSSManager para admin
|
||||||
|
*
|
||||||
|
* IMPORTANTE: Este Bootstrap registra el handler de formulario POST en admin_init,
|
||||||
|
* ANTES de que WordPress envíe headers HTTP. Esto permite que wp_redirect()
|
||||||
|
* funcione correctamente después de guardar/eliminar snippets.
|
||||||
|
*/
|
||||||
|
if (is_admin()) {
|
||||||
|
\ROITheme\Admin\CustomCSSManager\Infrastructure\Bootstrap\CustomCSSManagerBootstrap::init();
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 5.3. INFORMACIÓN DE DEBUG (Solo en desarrollo)
|
// 5.3. INFORMACIÓN DE DEBUG (Solo en desarrollo)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user