From f4b45b7e17a90619fc663e165e219537e8ac33ef Mon Sep 17 00:00:00 2001 From: FrankZamora Date: Wed, 3 Dec 2025 19:52:44 -0600 Subject: [PATCH] fix(exclusions): Corregir Renderers que ignoraban sistema de exclusiones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 99.11 - Correcciones críticas: - FooterRenderer: Añadir PageVisibilityHelper::shouldShow() - HeroSectionRenderer: Añadir PageVisibilityHelper::shouldShow() - AdsensePlacementRenderer: Añadir PageVisibilityHelper::shouldShow() Mejoras adicionales: - UrlPatternExclusion: Soporte wildcards (*sct* → regex) - ExclusionFormPartial: UI mejorada con placeholders - ComponentConfiguration: Grupo _exclusions validado - 12 FormBuilders: Integración UI de exclusiones - 12 FieldMappers: Mapeo campos de exclusión Verificado: Footer oculto en post con categoría excluida SCT 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../FieldMapping/ContactFormFieldMapper.php | 6 ++ .../Ui/ContactFormFormBuilder.php | 8 +++ .../FieldMapping/CtaLetsTalkFieldMapper.php | 6 ++ .../Ui/CtaLetsTalkFormBuilder.php | 8 +++ .../FieldMapping/CtaPostFieldMapper.php | 6 ++ .../Infrastructure/Ui/CtaPostFormBuilder.php | 8 +++ .../FieldMapping/FeaturedImageFieldMapper.php | 6 ++ .../Ui/FeaturedImageFormBuilder.php | 8 +++ .../FieldMapping/FooterFieldMapper.php | 13 ++++ .../Infrastructure/Ui/FooterFormBuilder.php | 57 +++++++++++++++++ .../FieldMapping/HeroFieldMapper.php | 6 ++ .../Infrastructure/Ui/HeroFormBuilder.php | 8 +++ .../FieldMapping/NavbarFieldMapper.php | 6 ++ .../Infrastructure/Ui/NavbarFormBuilder.php | 8 +++ .../FieldMapping/RelatedPostFieldMapper.php | 6 ++ .../Ui/RelatedPostFormBuilder.php | 8 +++ .../Ui/ExclusionFormPartial.php | 28 +++++++-- .../FieldMapping/SocialShareFieldMapper.php | 6 ++ .../Ui/SocialShareFormBuilder.php | 8 +++ .../TableOfContentsFieldMapper.php | 6 ++ .../Ui/TableOfContentsFormBuilder.php | 8 +++ .../TopNotificationBarFieldMapper.php | 6 ++ .../Ui/TopNotificationBarFormBuilder.php | 8 +++ .../Ui/AdsensePlacementRenderer.php | 6 ++ .../Infrastructure/Ui/FooterRenderer.php | 8 ++- .../Infrastructure/Ui/HeroSectionRenderer.php | 6 ++ .../ValueObjects/ComponentConfiguration.php | 3 + .../ValueObjects/UrlPatternExclusion.php | 38 ++++++++++- .../WordPressComponentSettingsRepository.php | 63 +++++++++++++++---- 29 files changed, 342 insertions(+), 20 deletions(-) diff --git a/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php b/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php index 20329657..48231fb5 100644 --- a/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php +++ b/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php @@ -34,6 +34,12 @@ final class ContactFormFieldMapper implements FieldMapperInterface 'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'contactFormExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'contactFormExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'contactFormExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'contactFormExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'], 'contactFormSectionDescription' => ['group' => 'content', 'attribute' => 'section_description'], diff --git a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php index 57cc938c..50e7cd9b 100644 --- a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php +++ b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\ContactForm\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * FormBuilder para Contact Form @@ -127,6 +128,13 @@ final class ContactFormFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'contactForm'); + $html .= ' '; $html .= ''; diff --git a/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php b/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php index c5cc4804..59289c66 100644 --- a/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php +++ b/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php @@ -34,6 +34,12 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface 'ctaLetsTalkVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'ctaLetsTalkVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'letsTalkExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'letsTalkExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'letsTalkExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'letsTalkExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'], 'ctaLetsTalkShowIcon' => ['group' => 'content', 'attribute' => 'show_icon'], diff --git a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php index fecea221..b56ed5d0 100644 --- a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php +++ b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * Class CtaLetsTalkFormBuilder @@ -154,6 +155,13 @@ final class CtaLetsTalkFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'letsTalk'); + $html .= ' '; $html .= ''; diff --git a/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php b/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php index 1c7f44ad..e7b0a0a6 100644 --- a/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php +++ b/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php @@ -34,6 +34,12 @@ final class CtaPostFieldMapper implements FieldMapperInterface 'ctaPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'ctaPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'ctaPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'ctaPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'ctaPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'ctaPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'], 'ctaPostDescription' => ['group' => 'content', 'attribute' => 'description'], diff --git a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php index 7406f633..91584103 100644 --- a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php +++ b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\CtaPost\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * FormBuilder para CTA Post @@ -119,6 +120,13 @@ final class CtaPostFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'ctaPost'); + $html .= ' '; $html .= ''; diff --git a/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php b/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php index b7a08d27..c897df31 100644 --- a/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php +++ b/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php @@ -34,6 +34,12 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface 'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'featuredImageExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'featuredImageExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'featuredImageExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'featuredImageExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'], 'featuredImageLazyLoading' => ['group' => 'content', 'attribute' => 'lazy_loading'], diff --git a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php index 6f47cb96..aa320d6d 100644 --- a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php +++ b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; final class FeaturedImageFormBuilder { @@ -134,6 +135,13 @@ final class FeaturedImageFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'featuredImage'); + $html .= ' '; $html .= ''; diff --git a/Admin/Footer/Infrastructure/FieldMapping/FooterFieldMapper.php b/Admin/Footer/Infrastructure/FieldMapping/FooterFieldMapper.php index 5e7bb3d5..938caf2f 100644 --- a/Admin/Footer/Infrastructure/FieldMapping/FooterFieldMapper.php +++ b/Admin/Footer/Infrastructure/FieldMapping/FooterFieldMapper.php @@ -27,6 +27,19 @@ final class FooterFieldMapper implements FieldMapperInterface 'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + // Page Visibility (grupo especial _page_visibility) + 'footerVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'footerVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'footerVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'footerVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'footerVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'footerExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'footerExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'footerExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'footerExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Widget 1 'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'], 'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'], diff --git a/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php b/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php index abb58b30..71f2ab35 100644 --- a/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php +++ b/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\Footer\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * FormBuilder para Footer @@ -90,6 +91,47 @@ final class FooterFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); $html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $html .= '
'; + $html .= '

'; + $html .= ' '; + $html .= ' Mostrar en tipos de pagina'; + $html .= '

'; + + $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true); + $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('footerVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('footerVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('footerVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('footerVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('footerVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; + $html .= '
'; + + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'footer'); + $html .= ' '; $html .= ''; @@ -410,4 +452,19 @@ final class FooterFormBuilder } return (string) $value; } + + private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string + { + $checked = $value === true || $value === '1' || $value === 1 ? 'checked' : ''; + + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + return $html; + } } diff --git a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php index 0fd229ed..49d54b58 100644 --- a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php +++ b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php @@ -35,6 +35,12 @@ final class HeroFieldMapper implements FieldMapperInterface 'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'heroExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'heroExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'heroExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'heroExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'], 'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'], diff --git a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php index d6fe0337..8ff8b6fe 100644 --- a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php +++ b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\Hero\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; final class HeroFormBuilder { @@ -136,6 +137,13 @@ final class HeroFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'hero'); + // Switch: CSS Crítico $isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true); $html .= '
'; diff --git a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php index af1f9244..fe3f1811 100644 --- a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php +++ b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php @@ -36,6 +36,12 @@ final class NavbarFieldMapper implements FieldMapperInterface 'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'navbarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'navbarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'navbarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'navbarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Layout 'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'], 'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'], diff --git a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php index f71f64fb..27c0da27 100644 --- a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php +++ b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\Navbar\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; final class NavbarFormBuilder { @@ -139,6 +140,13 @@ final class NavbarFormBuilder $html .= '
'; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'navbar'); + // Switch: Sticky $sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true); $html .= '
'; diff --git a/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php b/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php index 6bcae3ce..49b11f11 100644 --- a/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php +++ b/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php @@ -37,6 +37,12 @@ final class RelatedPostFieldMapper implements FieldMapperInterface 'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'relatedPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'relatedPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'relatedPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'relatedPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'], 'relatedPostPerPage' => ['group' => 'content', 'attribute' => 'posts_per_page'], diff --git a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php index 4cf3db80..2f36b2bb 100644 --- a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php +++ b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * FormBuilder para Related Posts @@ -120,6 +121,13 @@ final class RelatedPostFormBuilder $html .= '
'; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'relatedPost'); + $html .= ' '; $html .= ''; diff --git a/Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php b/Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php index dccedc69..2bae8641 100644 --- a/Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php +++ b/Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php @@ -210,11 +210,19 @@ final class ExclusionFormPartial } /** - * Convierte JSON array a lista separada por comas + * Convierte JSON array o array a lista separada por comas + * + * @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado) */ - private function jsonToCommaList(string $json): string + private function jsonToCommaList(string|array $value): string { - $decoded = json_decode($json, true); + // Si ya es array, usarlo directamente + if (is_array($value)) { + return empty($value) ? '' : implode(', ', $value); + } + + // Si es string, intentar decodificar JSON + $decoded = json_decode($value, true); if (!is_array($decoded) || empty($decoded)) { return ''; @@ -224,11 +232,19 @@ final class ExclusionFormPartial } /** - * Convierte JSON array a lista separada por lineas + * Convierte JSON array o array a lista separada por lineas + * + * @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado) */ - private function jsonToLineList(string $json): string + private function jsonToLineList(string|array $value): string { - $decoded = json_decode($json, true); + // Si ya es array, usarlo directamente + if (is_array($value)) { + return empty($value) ? '' : implode("\n", $value); + } + + // Si es string, intentar decodificar JSON + $decoded = json_decode($value, true); if (!is_array($decoded) || empty($decoded)) { return ''; diff --git a/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php b/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php index 827e9d70..e9b310cf 100644 --- a/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php +++ b/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php @@ -34,6 +34,12 @@ final class SocialShareFieldMapper implements FieldMapperInterface 'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'socialShareExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'socialShareExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'socialShareExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'socialShareExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'], 'socialShareLabelText' => ['group' => 'content', 'attribute' => 'label_text'], diff --git a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php index 7cf4741e..d91d9b7e 100644 --- a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php +++ b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\SocialShare\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * FormBuilder para Social Share @@ -128,6 +129,13 @@ final class SocialShareFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'socialShare'); + $html .= ' '; $html .= ''; diff --git a/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php b/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php index 8860fc5b..18652230 100644 --- a/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php +++ b/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php @@ -34,6 +34,12 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface 'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'tocExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'tocExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'tocExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'tocExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'tocTitle' => ['group' => 'content', 'attribute' => 'title'], 'tocAutoGenerate' => ['group' => 'content', 'attribute' => 'auto_generate'], diff --git a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php index c0866d1e..ef6af736 100644 --- a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php +++ b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; /** * FormBuilder para la Tabla de Contenido @@ -128,6 +129,13 @@ final class TableOfContentsFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'toc'); + $html .= ' '; $html .= ''; diff --git a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php index 041a5501..6099dc89 100644 --- a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php +++ b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php @@ -35,6 +35,12 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface 'topBarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], 'topBarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Exclusions (grupo especial _exclusions - Plan 99.11) + 'topBarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'], + 'topBarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'], + 'topBarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'], + 'topBarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'], + // Content 'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'], 'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'], diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php index 5fed2d32..4a3f3d21 100644 --- a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php +++ b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui; use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial; final class TopNotificationBarFormBuilder { @@ -139,6 +140,13 @@ final class TopNotificationBarFormBuilder $html .= ' '; $html .= ' '; + // ============================================= + // Reglas de exclusion avanzadas + // Grupo especial: _exclusions (Plan 99.11) + // ============================================= + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'topBar'); + // Switch: CSS Crítico $isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true); $html .= '
'; diff --git a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php index 922993b6..fdd56860 100644 --- a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php +++ b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ROITheme\Public\AdsensePlacement\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; +use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper; /** * Renderer para slots de AdSense @@ -36,6 +37,11 @@ final class AdsensePlacementRenderer */ public function renderSlot(array $settings, string $location): string { + // 0. Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11) + if (!PageVisibilityHelper::shouldShow('adsense-placement')) { + return ''; + } + // 1. Validar is_enabled if (!($settings['visibility']['is_enabled'] ?? false)) { return ''; diff --git a/Public/Footer/Infrastructure/Ui/FooterRenderer.php b/Public/Footer/Infrastructure/Ui/FooterRenderer.php index b304ef27..e295146f 100644 --- a/Public/Footer/Infrastructure/Ui/FooterRenderer.php +++ b/Public/Footer/Infrastructure/Ui/FooterRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\Footer\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; use ROITheme\Shared\Domain\Entities\Component; +use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper; /** * FooterRenderer - Renderiza el footer del sitio @@ -34,9 +35,14 @@ final class FooterRenderer implements RendererInterface public function render(Component $component): string { + // Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11) + if (!PageVisibilityHelper::shouldShow('footer')) { + return ''; + } + $data = $component->getData(); - // Validar visibilidad + // Validar visibilidad básica $visibility = $data['visibility'] ?? []; if (!($visibility['is_enabled'] ?? true)) { return ''; diff --git a/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php b/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php index 72936fc4..680da217 100644 --- a/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php +++ b/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php @@ -5,6 +5,7 @@ namespace ROITheme\Public\HeroSection\Infrastructure\Ui; use ROITheme\Shared\Domain\Entities\Component; use ROITheme\Shared\Domain\Contracts\RendererInterface; +use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper; /** * HeroSectionRenderer - Renderiza la sección hero con badges y título @@ -23,6 +24,11 @@ final class HeroSectionRenderer implements RendererInterface { public function render(Component $component): string { + // Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11) + if (!PageVisibilityHelper::shouldShow('hero-section')) { + return ''; + } + $data = $component->getData(); if (!$this->isEnabled($data)) { diff --git a/Shared/Domain/ValueObjects/ComponentConfiguration.php b/Shared/Domain/ValueObjects/ComponentConfiguration.php index 3801287b..d6914f7e 100644 --- a/Shared/Domain/ValueObjects/ComponentConfiguration.php +++ b/Shared/Domain/ValueObjects/ComponentConfiguration.php @@ -96,6 +96,9 @@ final readonly class ComponentConfiguration // Sistema de visibilidad por página '_page_visibility', // Visibilidad por tipo de página (home, posts, pages, archives, search) + + // Sistema de exclusiones (Plan 99.11) + '_exclusions', // Reglas de exclusión por categoría, post ID, URL pattern ]; /** diff --git a/Shared/Domain/ValueObjects/UrlPatternExclusion.php b/Shared/Domain/ValueObjects/UrlPatternExclusion.php index c7aa9f13..486f3f21 100644 --- a/Shared/Domain/ValueObjects/UrlPatternExclusion.php +++ b/Shared/Domain/ValueObjects/UrlPatternExclusion.php @@ -86,10 +86,21 @@ final class UrlPatternExclusion extends ExclusionRule } /** - * Evalua coincidencia por substring + * Evalua coincidencia por substring o wildcard + * + * Soporta wildcards simples: + * - `*sct*` coincide con URLs que contengan "sct" + * - `*` se convierte a `.*` en regex + * - Sin wildcards: busca substring literal */ private function matchesSubstring(string $pattern, string $requestUri, string $url): bool { + // Detectar si tiene wildcards (*) + if (str_contains($pattern, '*')) { + return $this->matchesWildcard($pattern, $requestUri, $url); + } + + // Substring literal if ($requestUri !== '' && str_contains($requestUri, $pattern)) { return true; } @@ -101,6 +112,31 @@ final class UrlPatternExclusion extends ExclusionRule return false; } + /** + * Evalua coincidencia con patron wildcard + * + * Convierte wildcards (*) a regex (.*) + */ + private function matchesWildcard(string $pattern, string $requestUri, string $url): bool + { + // Convertir wildcard a regex: + // 1. Escapar caracteres especiales de regex (excepto *) + // 2. Convertir * a .* + $regexPattern = preg_quote($pattern, '#'); + $regexPattern = str_replace('\\*', '.*', $regexPattern); + $regexPattern = '#' . $regexPattern . '#i'; + + if ($requestUri !== '' && preg_match($regexPattern, $requestUri) === 1) { + return true; + } + + if ($url !== '' && preg_match($regexPattern, $url) === 1) { + return true; + } + + return false; + } + public function hasValues(): bool { return !empty($this->urlPatterns); diff --git a/Shared/Infrastructure/Persistence/WordPress/WordPressComponentSettingsRepository.php b/Shared/Infrastructure/Persistence/WordPress/WordPressComponentSettingsRepository.php index 2162e938..4e9dd387 100644 --- a/Shared/Infrastructure/Persistence/WordPress/WordPressComponentSettingsRepository.php +++ b/Shared/Infrastructure/Persistence/WordPress/WordPressComponentSettingsRepository.php @@ -109,28 +109,67 @@ final class WordPressComponentSettingsRepository implements ComponentSettingsRep /** * {@inheritDoc} + * + * Implementa UPSERT: si el registro no existe, lo crea; si existe, lo actualiza. + * Esto es necesario para grupos especiales como _page_visibility y _exclusions + * que no vienen del schema JSON. */ public function saveFieldValue(string $componentName, string $groupName, string $attributeName, mixed $value): bool { // Serializar valor $serializedValue = $this->serializeValue($value); - // Intentar actualizar - $result = $this->wpdb->update( - $this->tableName, - ['attribute_value' => $serializedValue], - [ - 'component_name' => $componentName, - 'group_name' => $groupName, - 'attribute_name' => $attributeName - ], - ['%s'], - ['%s', '%s', '%s'] - ); + // Verificar si el registro existe + $exists = $this->fieldExists($componentName, $groupName, $attributeName); + + if ($exists) { + // UPDATE + $result = $this->wpdb->update( + $this->tableName, + ['attribute_value' => $serializedValue], + [ + 'component_name' => $componentName, + 'group_name' => $groupName, + 'attribute_name' => $attributeName + ], + ['%s'], + ['%s', '%s', '%s'] + ); + } else { + // INSERT - crear nuevo registro + $result = $this->wpdb->insert( + $this->tableName, + [ + 'component_name' => $componentName, + 'group_name' => $groupName, + 'attribute_name' => $attributeName, + 'attribute_value' => $serializedValue + ], + ['%s', '%s', '%s', '%s'] + ); + } return $result !== false; } + /** + * Verifica si un campo existe en la BD + */ + private function fieldExists(string $componentName, string $groupName, string $attributeName): bool + { + $sql = $this->wpdb->prepare( + "SELECT COUNT(*) FROM {$this->tableName} + WHERE component_name = %s + AND group_name = %s + AND attribute_name = %s", + $componentName, + $groupName, + $attributeName + ); + + return (int) $this->wpdb->get_var($sql) > 0; + } + /** * {@inheritDoc} */