Adds ability to exclude components from specific: - Categories (by slug or term_id) - Post/Page IDs - URL patterns (substring or regex) Architecture: - Domain: Value Objects (CategoryExclusion, PostIdExclusion, UrlPatternExclusion, ExclusionRuleSet) + Contracts - Application: EvaluateExclusionsUseCase + EvaluateComponentVisibilityUseCase (orchestrator) - Infrastructure: WordPressExclusionRepository, WordPressPageContextProvider, WordPressServerRequestProvider - Admin: ExclusionFormPartial (reusable UI), ExclusionFieldProcessor, JS toggle The PageVisibilityHelper now uses the orchestrator UseCase that combines page-type visibility (Plan 99.10) with exclusion rules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
245 lines
7.6 KiB
PHP
245 lines
7.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
|
|
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
|
|
|
/**
|
|
* Componente UI parcial reutilizable para reglas de exclusion
|
|
*
|
|
* Genera el HTML para la seccion de exclusiones en FormBuilders.
|
|
* Debe ser incluido despues de la seccion de visibilidad por tipo de pagina.
|
|
*
|
|
* Uso en FormBuilder:
|
|
* ```php
|
|
* $exclusionPartial = new ExclusionFormPartial($this->renderer);
|
|
* $html .= $exclusionPartial->render($componentId, 'prefijo');
|
|
* ```
|
|
*
|
|
* @package ROITheme\Admin\Shared\Infrastructure\Ui
|
|
*/
|
|
final class ExclusionFormPartial
|
|
{
|
|
private const GROUP_NAME = '_exclusions';
|
|
|
|
public function __construct(
|
|
private readonly AdminDashboardRenderer $renderer
|
|
) {}
|
|
|
|
/**
|
|
* Renderiza la seccion de exclusiones
|
|
*
|
|
* @param string $componentId ID del componente (kebab-case)
|
|
* @param string $prefix Prefijo para IDs de campos (ej: 'cta' genera 'ctaExclusionsEnabled')
|
|
* @return string HTML de la seccion
|
|
*/
|
|
public function render(string $componentId, string $prefix): string
|
|
{
|
|
$html = '';
|
|
|
|
$html .= $this->buildExclusionHeader();
|
|
$html .= $this->buildExclusionToggle($componentId, $prefix);
|
|
$html .= $this->buildExclusionRules($componentId, $prefix);
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildExclusionHeader(): string
|
|
{
|
|
$html = '<hr class="my-3">';
|
|
$html .= '<p class="small fw-semibold mb-2">';
|
|
$html .= ' <i class="bi bi-funnel me-1" style="color: #FF8600;"></i>';
|
|
$html .= ' Reglas de exclusion avanzadas';
|
|
$html .= '</p>';
|
|
$html .= '<p class="small text-muted mb-2">';
|
|
$html .= ' Excluir este componente de categorias, posts o URLs especificos.';
|
|
$html .= '</p>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildExclusionToggle(string $componentId, string $prefix): string
|
|
{
|
|
$enabled = $this->renderer->getFieldValue(
|
|
$componentId,
|
|
self::GROUP_NAME,
|
|
'exclusions_enabled',
|
|
false
|
|
);
|
|
$checked = $this->toBool($enabled);
|
|
|
|
$id = $prefix . 'ExclusionsEnabled';
|
|
|
|
$html = '<div class="mb-3">';
|
|
$html .= ' <div class="form-check form-switch">';
|
|
$html .= sprintf(
|
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
|
esc_attr($id),
|
|
$checked ? 'checked' : ''
|
|
);
|
|
$html .= sprintf(
|
|
' <label class="form-check-label small" for="%s">',
|
|
esc_attr($id)
|
|
);
|
|
$html .= ' <i class="bi bi-filter-circle me-1" style="color: #FF8600;"></i>';
|
|
$html .= ' <strong>Activar reglas de exclusion</strong>';
|
|
$html .= ' </label>';
|
|
$html .= ' </div>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildExclusionRules(string $componentId, string $prefix): string
|
|
{
|
|
$enabled = $this->renderer->getFieldValue(
|
|
$componentId,
|
|
self::GROUP_NAME,
|
|
'exclusions_enabled',
|
|
false
|
|
);
|
|
$display = $this->toBool($enabled) ? 'block' : 'none';
|
|
|
|
$html = sprintf(
|
|
'<div id="%sExclusionRules" style="display: %s;">',
|
|
esc_attr($prefix),
|
|
$display
|
|
);
|
|
|
|
$html .= $this->buildCategoryField($componentId, $prefix);
|
|
$html .= $this->buildPostIdsField($componentId, $prefix);
|
|
$html .= $this->buildUrlPatternsField($componentId, $prefix);
|
|
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildCategoryField(string $componentId, string $prefix): string
|
|
{
|
|
$value = $this->renderer->getFieldValue(
|
|
$componentId,
|
|
self::GROUP_NAME,
|
|
'exclude_categories',
|
|
'[]'
|
|
);
|
|
$categories = $this->jsonToCommaList($value);
|
|
|
|
$id = $prefix . 'ExcludeCategories';
|
|
|
|
$html = '<div class="mb-3">';
|
|
$html .= sprintf(
|
|
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
|
esc_attr($id)
|
|
);
|
|
$html .= ' <i class="bi bi-folder me-1" style="color: #FF8600;"></i>';
|
|
$html .= ' Excluir en categorias';
|
|
$html .= ' </label>';
|
|
$html .= sprintf(
|
|
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="noticias, eventos, tutoriales">',
|
|
esc_attr($id),
|
|
esc_attr($categories)
|
|
);
|
|
$html .= ' <small class="text-muted">Slugs de categorias separados por comas</small>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildPostIdsField(string $componentId, string $prefix): string
|
|
{
|
|
$value = $this->renderer->getFieldValue(
|
|
$componentId,
|
|
self::GROUP_NAME,
|
|
'exclude_post_ids',
|
|
'[]'
|
|
);
|
|
$postIds = $this->jsonToCommaList($value);
|
|
|
|
$id = $prefix . 'ExcludePostIds';
|
|
|
|
$html = '<div class="mb-3">';
|
|
$html .= sprintf(
|
|
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
|
esc_attr($id)
|
|
);
|
|
$html .= ' <i class="bi bi-hash me-1" style="color: #FF8600;"></i>';
|
|
$html .= ' Excluir en posts/paginas';
|
|
$html .= ' </label>';
|
|
$html .= sprintf(
|
|
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="123, 456, 789">',
|
|
esc_attr($id),
|
|
esc_attr($postIds)
|
|
);
|
|
$html .= ' <small class="text-muted">IDs de posts o paginas separados por comas</small>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildUrlPatternsField(string $componentId, string $prefix): string
|
|
{
|
|
$value = $this->renderer->getFieldValue(
|
|
$componentId,
|
|
self::GROUP_NAME,
|
|
'exclude_url_patterns',
|
|
'[]'
|
|
);
|
|
$patterns = $this->jsonToLineList($value);
|
|
|
|
$id = $prefix . 'ExcludeUrlPatterns';
|
|
|
|
$html = '<div class="mb-0">';
|
|
$html .= sprintf(
|
|
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
|
esc_attr($id)
|
|
);
|
|
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
|
$html .= ' Excluir por patrones URL';
|
|
$html .= ' </label>';
|
|
$html .= sprintf(
|
|
' <textarea id="%s" class="form-control form-control-sm" rows="3" placeholder="/privado/ /landing-especial/ /^\/categoria\/\d+$/">%s</textarea>',
|
|
esc_attr($id),
|
|
esc_textarea($patterns)
|
|
);
|
|
$html .= ' <small class="text-muted">Un patron por linea. Soporta texto simple o regex (ej: /^\/blog\/\d+$/)</small>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Convierte JSON array a lista separada por comas
|
|
*/
|
|
private function jsonToCommaList(string $json): string
|
|
{
|
|
$decoded = json_decode($json, true);
|
|
|
|
if (!is_array($decoded) || empty($decoded)) {
|
|
return '';
|
|
}
|
|
|
|
return implode(', ', $decoded);
|
|
}
|
|
|
|
/**
|
|
* Convierte JSON array a lista separada por lineas
|
|
*/
|
|
private function jsonToLineList(string $json): string
|
|
{
|
|
$decoded = json_decode($json, true);
|
|
|
|
if (!is_array($decoded) || empty($decoded)) {
|
|
return '';
|
|
}
|
|
|
|
return implode("\n", $decoded);
|
|
}
|
|
|
|
private function toBool(mixed $value): bool
|
|
{
|
|
return $value === true || $value === '1' || $value === 1;
|
|
}
|
|
}
|