diff --git a/Admin/CustomCSSManager/Infrastructure/Bootstrap/CustomCSSManagerBootstrap.php b/Admin/CustomCSSManager/Infrastructure/Bootstrap/CustomCSSManagerBootstrap.php new file mode 100644 index 00000000..4554cb71 --- /dev/null +++ b/Admin/CustomCSSManager/Infrastructure/Bootstrap/CustomCSSManagerBootstrap.php @@ -0,0 +1,107 @@ + 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); + } +} diff --git a/Admin/CustomCSSManager/Infrastructure/Ui/CustomCSSManagerFormBuilder.php b/Admin/CustomCSSManager/Infrastructure/Ui/CustomCSSManagerFormBuilder.php index 93e49e62..77234a02 100644 --- a/Admin/CustomCSSManager/Infrastructure/Ui/CustomCSSManagerFormBuilder.php +++ b/Admin/CustomCSSManager/Infrastructure/Ui/CustomCSSManagerFormBuilder.php @@ -5,12 +5,7 @@ namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; 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\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 @@ -19,6 +14,9 @@ use ROITheme\Shared\Domain\Exceptions\ValidationException; * - Constructor recibe AdminDashboardRenderer * - 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 */ final class CustomCSSManagerFormBuilder @@ -28,120 +26,15 @@ final class CustomCSSManagerFormBuilder private WordPressSnippetRepository $repository; private GetAllSnippetsUseCase $getAllUseCase; - private SaveSnippetUseCase $saveUseCase; - private DeleteSnippetUseCase $deleteUseCase; public function __construct( private readonly AdminDashboardRenderer $renderer ) { - // Crear repositorio y Use Cases internamente + // Crear repositorio y Use Case para listar snippets global $wpdb; $this->repository = new WordPressSnippetRepository($wpdb); $this->getAllUseCase = new GetAllSnippetsUseCase($this->repository); - $this->saveUseCase = new SaveSnippetUseCase($this->repository); - $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); + // NOTA: El handler POST está en CustomCSSManagerBootstrap (admin_init) } /** @@ -160,13 +53,9 @@ final class CustomCSSManagerFormBuilder // Header $html .= $this->buildHeader($componentId, count($snippets)); - // Mensajes flash + // Toast para mensajes (usa el sistema existente de admin-dashboard.js) if ($message) { - $html .= sprintf( - '
%s
', - esc_attr($message['type']), - esc_html($message['text']) - ); + $html .= $this->buildToastTrigger($message); } // Lista de snippets existentes @@ -367,7 +256,7 @@ final class CustomCSSManagerFormBuilder // Botones $html .= '
'; $html .= ' '; $html .= ' +
+ + `; + + 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()); + }); + + HTML; + } } diff --git a/Admin/Infrastructure/Ui/Views/partials/component-view.php b/Admin/Infrastructure/Ui/Views/partials/component-view.php index e6fb0493..92683531 100644 --- a/Admin/Infrastructure/Ui/Views/partials/component-view.php +++ b/Admin/Infrastructure/Ui/Views/partials/component-view.php @@ -60,6 +60,12 @@ $group = $groupId && isset($groups[$groupId]) ? $groups[$groupId] : null; +
+ diff --git a/functions.php b/functions.php index ab25190e..97040a0b 100644 --- a/functions.php +++ b/functions.php @@ -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) // =============================================================================