<?php
|
namespace JVBase\managers;
|
|
use JVBase\utility\Features;
|
use WP_REST_Response;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* AdminManager - Handles Edmonton Ink admin pages in the WordPress dashboard
|
*
|
* Creates a main settings page with the ability for other classes to register subpages
|
*/
|
class AdminPages
|
{
|
protected array $subpages;
|
protected array $actions;
|
protected string $page_hook;
|
protected string $prefix;
|
protected array $main_page;
|
/**
|
* Constructor
|
*/
|
public function __construct()
|
{
|
// Set up main page details
|
$this->main_page = [
|
'page_title' => 'JakeVan Settings',
|
'menu_title' => 'JakeVan',
|
'capability' => 'manage_options',
|
'menu_slug' => BASE . 'settings',
|
'icon' => jvbCSSIcon('settings'),
|
'position' => 0
|
];
|
$this->subpages = apply_filters('jvbAdminSubpages', []);
|
// delete_option(BASE.'admin_actions');
|
// delete_option(BASE.'admin_subpages');
|
// $this->getSubpages();
|
|
$this->getActions();
|
|
// Hook into WordPress admin
|
add_action('admin_menu', [$this, 'registerAdminPages']);
|
add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);
|
|
add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
|
|
add_action('rest_api_init', [$this, 'registerRestRoutes']);
|
}
|
|
/**
|
* Register REST API routes for admin actions
|
*/
|
public function registerRestRoutes(): void
|
{
|
register_rest_route('jvb/v1', '/admin-cache', [
|
'methods' => 'POST',
|
'callback' => [$this, 'handleCacheAction'],
|
'permission_callback' => [$this, 'checkAdminPermission']
|
]);
|
|
register_rest_route('jvb/v1', '/admin-icons', [
|
'methods' => 'POST',
|
'callback' => [$this, 'handleIconAction'],
|
'permission_callback' => [$this, 'checkAdminPermission']
|
]);
|
}
|
|
/**
|
* Check if user has admin permissions
|
*/
|
public function checkAdminPermission(\WP_REST_Request $request): bool
|
{
|
if (!current_user_can('manage_options')) {
|
return false;
|
}
|
|
// Verify nonce
|
$nonce = $request->get_header('X-WP-Nonce');
|
if (!wp_verify_nonce($nonce, 'wp_rest')) {
|
return false;
|
}
|
|
return true;
|
}
|
|
/**
|
* Handle cache-related actions
|
*/
|
public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
|
{
|
$action = sanitize_text_field($request->get_param('action'));
|
|
switch ($action) {
|
case 'flush-all':
|
wp_cache_flush();
|
return new \WP_REST_Response([
|
'success' => true,
|
'message' => 'All caches flushed successfully'
|
]);
|
|
case 'flush-cache':
|
$group = sanitize_text_field($request->get_param('group'));
|
if (empty($group)) {
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'No cache group specified'
|
], 400);
|
}
|
|
\JVBase\managers\CacheManager::invalidateAll($group);
|
|
return new \WP_REST_Response([
|
'success' => true,
|
'message' => "Cache group '{$group}' flushed successfully"
|
]);
|
|
default:
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'Invalid action'
|
], 400);
|
}
|
}
|
|
/**
|
* Handle icon-related actions
|
*/
|
public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
|
{
|
$action = sanitize_text_field($request->get_param('action'));
|
$icons = \JVBase\managers\IconsManager::getInstance();
|
|
switch ($action) {
|
case 'refresh-icons':
|
$icons->forceRefresh();
|
return new \WP_REST_Response([
|
'success' => true,
|
'message' => 'Icon CSS regenerated successfully'
|
]);
|
|
case 'restore-icon-version':
|
$timestamp = (int)$request->get_param('timestamp');
|
if (empty($timestamp)) {
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'No timestamp provided'
|
], 400);
|
}
|
|
if ($icons->restoreVersion($timestamp)) {
|
return new \WP_REST_Response([
|
'success' => true,
|
'message' => 'Icon version restored successfully'
|
]);
|
}
|
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'Failed to restore icon version'
|
], 500);
|
|
case 'merge-icon-versions':
|
$timestamps = $request->get_param('timestamps');
|
|
if (empty($timestamps) || !is_array($timestamps)) {
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'No versions selected for merging'
|
], 400);
|
}
|
|
$timestamps = array_map('intval', $timestamps);
|
|
if (count($timestamps) < 2) {
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'Please select at least 2 versions to merge'
|
], 400);
|
}
|
|
if ($icons->mergeVersions($timestamps)) {
|
return new \WP_REST_Response([
|
'success' => true,
|
'message' => 'Icon versions merged successfully'
|
]);
|
}
|
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'Failed to merge icon versions'
|
], 500);
|
|
default:
|
return new \WP_REST_Response([
|
'success' => false,
|
'message' => 'Invalid action'
|
], 400);
|
}
|
}
|
|
/**
|
* Register a subpage to appear under the main settings page
|
*
|
* @param string $page_title The text to be displayed in the title tags of the page
|
* @param string $menu_title The text to be used for the menu
|
* @param string $capability The capability required to access the page
|
* @param string $menu_slug The slug name to refer to this menu by
|
* @param callable $callback The function to be called to output the content for this page
|
* @param string|null $icon
|
* @param int|null $position The position in the menu order (optional)
|
* @return bool Success or failure
|
*/
|
public function registerSubpage(
|
string $page_title,
|
string $menu_title,
|
string $capability,
|
string $menu_slug,
|
callable $callback,
|
string|null $icon = null,
|
int|null $position = null
|
):bool {
|
return true;
|
// if (empty($page_title) ||
|
// empty($menu_title) ||
|
// empty($capability) ||
|
// empty($menu_slug) ||
|
// !is_callable($callback)) {
|
// error_log('This wasn\'t registered: '.print_r([
|
// 'page_title' => $page_title,
|
// 'menu_title' => $menu_title,
|
// 'capability' => $capability,
|
// 'menu_slug' => $menu_slug,
|
// 'callback' => $callback
|
// ], true));
|
// return false;
|
// }
|
//
|
// $this->getSubpages();
|
//
|
// $subpage = [
|
// 'page_title' => $page_title,
|
// 'menu_title' => $menu_title,
|
// 'capability' => $capability,
|
// 'menu_slug' => BASE . $menu_slug,
|
// 'callback' => $callback,
|
// 'position' => $position
|
// ];
|
//
|
// if (!is_null($position)) {
|
// $subpage['position'] = $position;
|
// }
|
// if (!is_null($icon)) {
|
// $subpage['icon'] = $this->getIcon($icon);
|
// }
|
//
|
// $this->subpages[] = $subpage;
|
//
|
// $this->setSubpages();
|
//
|
// return true;
|
}
|
|
protected function getSubpages()
|
{
|
$this->subpages = get_option(BASE.'admin_subpages', []);
|
}
|
|
protected function setSubpages()
|
{
|
update_option(BASE.'admin_subpages', $this->subpages);
|
}
|
|
public function registerAction(
|
string $label,
|
string $slug,
|
string $capability,
|
string|null $icon = null
|
):bool {
|
$this->getActions();
|
$action = [
|
'label' => $label,
|
'slug' => sanitize_title($slug),
|
'capability'=> $capability,
|
];
|
if (!is_null($icon)) {
|
$action['icon'] = $icon;
|
}
|
$this->actions[sanitize_title($slug)] = $action;
|
$this->setActions();
|
return true;
|
}
|
|
protected function getActions()
|
{
|
$this->actions = get_option(BASE.'admin_actions', []);
|
}
|
|
protected function setActions()
|
{
|
update_option(BASE.'admin_actions', $this->actions);
|
}
|
/**
|
* Register admin pages with WordPress
|
*/
|
public function registerAdminPages():void
|
{
|
// Add main settings page
|
$this->page_hook = add_menu_page(
|
$this->main_page['page_title'],
|
$this->main_page['menu_title'],
|
$this->main_page['capability'],
|
$this->main_page['menu_slug'],
|
[$this, 'renderMainPage'],
|
$this->main_page['icon'],
|
$this->main_page['position']
|
);
|
|
// Add main page as a subpage to make it appear in submenu
|
add_submenu_page(
|
$this->main_page['menu_slug'],
|
$this->main_page['page_title'],
|
'Overview',
|
$this->main_page['capability'],
|
$this->main_page['menu_slug']
|
);
|
|
add_submenu_page(
|
$this->main_page['menu_slug'],
|
'Cache',
|
'Cache',
|
'manage_options',
|
BASE.'cache',
|
[$this, 'renderCachePage']
|
);
|
add_submenu_page(
|
$this->main_page['menu_slug'],
|
'Icon Management',
|
'Icons',
|
'manage_options',
|
BASE.'icons',
|
[$this, 'renderIconsPage']
|
);
|
|
// $this->getSubpages();
|
// Add registered subpages
|
foreach ($this->subpages as $page) {
|
if (!array_key_exists('page_title', $page) ||
|
!array_key_exists('menu_title', $page) ||
|
!array_key_exists('capability', $page) ||
|
!array_key_exists('menu_slug', $page) ||
|
!array_key_exists('callback', $page)) {
|
error_log('Invalid admin page config for '.print_r($page, true));
|
continue;
|
}
|
add_submenu_page(
|
$this->main_page['menu_slug'],
|
$page['page_title'],
|
$page['menu_title'],
|
$page['capability'],
|
$page['menu_slug'],
|
$page['callback']
|
);
|
}
|
}
|
|
/**
|
* Render the main settings page
|
*/
|
public function renderMainPage():void
|
{
|
?>
|
<div class="wrap jvb-admin-wrap">
|
<h1><?= esc_html($this->main_page['page_title']); ?></h1>
|
|
<div class="jvb-admin-content">
|
<?php $this->renderDashboard(); ?>
|
</div>
|
</div>
|
<?php
|
}
|
|
/**
|
* Render the main dashboard overview
|
*/
|
protected function renderDashboard():void
|
{
|
?>
|
<div class="jvb-dashboard-grid">
|
<div class="jvb-dashboard-card">
|
<h2>System Status</h2>
|
<ul class="jvb-status-list">
|
<?php $this->renderStatusItems(); ?>
|
</ul>
|
</div>
|
|
<div class="jvb-dashboard-card">
|
<h2>Active Users</h2>
|
<div class="jvb-active-users">
|
<?php $this->renderUserStats(); ?>
|
</div>
|
</div>
|
|
<div class="jvb-dashboard-card">
|
<h2>Recent Content</h2>
|
<div class="jvb-recent-content">
|
<?php $this->renderRecentContent(); ?>
|
</div>
|
</div>
|
|
<div class="jvb-dashboard-card jvb-quick-links">
|
<h2>Quick Actions</h2>
|
<div class="jvb-link-grid">
|
<?php $this->renderActions(); ?>
|
</div>
|
</div>
|
|
<div class="jvb-dashboard-card jvb-quick-links">
|
<h2>Quick Links</h2>
|
<div class="jvb-link-grid">
|
<?php $this->renderQuickLinks(); ?>
|
</div>
|
</div>
|
</div>
|
<?php
|
|
}
|
|
/**
|
* Render system status information
|
*/
|
protected function renderStatusItems():void
|
{
|
// Get queue stats
|
$queue_status = JVB()->queue()->getQueueStatus();
|
error_log('Queue Status: '.print_r($queue_status, true));
|
|
// Other system checks
|
$memory_usage = memory_get_usage(true) / 1024 / 1024;
|
|
// Display status items
|
?>
|
<li>
|
<span class="status-label">Queue Status:</span>
|
<span class="status-value">
|
<?= (isset($queue_status['pending']) ? esc_html($queue_status['pending']) : '0'); ?> pending,
|
<?= (isset($queue_status['processing']) ?
|
esc_html($queue_status['processing']) :
|
'0'); ?> processing
|
</span>
|
</li>
|
<li>
|
<span class="status-label">Memory Usage:</span>
|
<span class="status-value"><?= round($memory_usage, 2); ?> MB</span>
|
</li>
|
<li>
|
<span class="status-label">Plugin Version:</span>
|
<span class="status-value">1.0.0</span>
|
</li>
|
<li>
|
<span class="status-label">Content Types:</span>
|
<span class="status-value"><?= count(JVB_CONTENT); ?> registered</span>
|
</li>
|
<li>
|
<span class="status-label">Taxonomies:</span>
|
<span class="status-value"><?= count(JVB_TAXONOMY); ?> registered</span>
|
</li>
|
<?php
|
}
|
|
/**
|
* Render user statistics
|
*/
|
protected function renderUserStats():void
|
{
|
$roles = get_editable_roles();
|
|
foreach ($roles as $role => $config) {
|
$count = count_users()['avail_roles'][$role]??0;
|
?>
|
<div class="jvb-user-stat">
|
<span class="jvb-stat-number"><?= esc_html($count); ?></span>
|
<span class="jvb-stat-label"><?=$config['name']?></span>
|
</div>
|
<?php
|
}
|
}
|
|
/**
|
* Render recent content statistics
|
*/
|
protected function renderRecentContent():void
|
{
|
// Get content counts from the last 7 days
|
global $wpdb;
|
|
$week_ago = date('Y-m-d H:i:s', strtotime('-7 days'));
|
$content_types = [];
|
foreach (JVB_CONTENT as $content => $config) {
|
$content_types[jvbCheckBase($content)] = $config['plural'];
|
}
|
|
?>
|
<table class="jvb-content-table">
|
<tr>
|
<th>Content Type</th>
|
<th>Last 7 Days</th>
|
<th>Total</th>
|
</tr>
|
<?php foreach ($content_types as $type => $label) : ?>
|
<?php
|
$recent_count = $wpdb->get_var($wpdb->prepare(
|
"SELECT COUNT(*) FROM {$wpdb->posts}
|
WHERE post_type = %s
|
AND post_date > %s
|
AND post_status = 'publish'",
|
$type,
|
$week_ago
|
));
|
|
$total_count = $wpdb->get_var($wpdb->prepare(
|
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'",
|
$type
|
));
|
?>
|
<tr>
|
<td><?= esc_html($label); ?></td>
|
<td><?= esc_html($recent_count); ?></td>
|
<td><?= esc_html($total_count); ?></td>
|
</tr>
|
<?php endforeach; ?>
|
</table>
|
<?php
|
}
|
|
/**
|
* Render quick links for common admin tasks
|
*/
|
protected function renderQuickLinks():void
|
{
|
$links = [
|
[
|
'title' => 'Moderate Artist Requests',
|
'url' => admin_url('admin.php?page=' . BASE . 'artist_approval'),
|
'icon' => 'dashicons-id'
|
],
|
[
|
'title' => 'Manage Shops',
|
'url' => admin_url('edit-tags.php?taxonomy=' . BASE . 'shop'),
|
'icon' => 'dashicons-store'
|
],
|
[
|
'title' => 'Queue Status',
|
'url' => admin_url('admin.php?page=' . BASE . 'queue_status'),
|
'icon' => 'dashicons-list-view'
|
],
|
[
|
'title' => 'Manage Styles',
|
'url' => admin_url('edit-tags.php?taxonomy=' . BASE . 'style'),
|
'icon' => 'dashicons-admin-customizer'
|
]
|
];
|
|
foreach ($links as $link) {
|
?>
|
<a href="<?= esc_url($link['url']); ?>" class="jvb-quick-link">
|
<span class="dashicons <?= esc_attr($link['icon']); ?>"></span>
|
<span class="jvb-link-title"><?= esc_html($link['title']); ?></span>
|
</a>
|
<?php
|
}
|
}
|
|
protected function renderActions()
|
{
|
foreach ($this->actions as $action) {
|
if (current_user_can($action['capability'])) {
|
?>
|
<a data-action="<?=$action['slug']?>" class="jvb-action">
|
<?= jvbIcon($action['icon']); ?>
|
<span class="jvb-link-title"><?= esc_html($action['label'])?></span>
|
<span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span>
|
</a>
|
<?php
|
}
|
}
|
}
|
|
/**
|
* Enqueue admin assets (CSS and JS)
|
*
|
* @param string $hook Current admin page
|
*/
|
public function enqueueAdminAssets(string $hook):void
|
{
|
// Check if we're on an Edmonton Ink admin page
|
if (strpos($hook, BASE) === false) {
|
return;
|
}
|
|
// Enqueue admin styles
|
wp_enqueue_style(
|
'jvb-admin-styles',
|
JVB_URL . 'assets/css/admin.css',
|
[],
|
'1.0.0'
|
);
|
|
// Enqueue admin scripts
|
wp_enqueue_script(
|
'jvb-admin-scripts',
|
JVB_URL . 'assets/js/admin.js',
|
[],
|
'1.0.0',
|
true
|
);
|
|
wp_localize_script(
|
'jvb-admin-scripts',
|
'jvbSettings',
|
[
|
'api' => rest_url('jvb/v1/admin-action'),
|
'nonce' => wp_create_nonce('wp_rest'),
|
'action' => wp_create_nonce('itsme'),
|
]
|
);
|
}
|
|
/**
|
* Create a custom SVG icon for the admin menu
|
* @param string $icon Icon name
|
* @param bool $css Whether to convert to CSS
|
* @return string Base64 encoded SVG icon
|
*/
|
protected function getIcon(string $icon = 'logo', bool $css = false): string
|
{
|
$svg = jvbIcon($icon, ['wrap' => false]);
|
if ($css) {
|
// For CSS, replace currentColor with brand color
|
$svg = str_replace('currentColor', '#FF0080', $svg);
|
|
// Clean and prepare the SVG for CSS
|
$svg = preg_replace('/[\r\n\t]+/', ' ', $svg); // Remove newlines and tabs
|
$svg = preg_replace('/\s{2,}/', ' ', $svg); // Minimize spaces
|
$svg = trim($svg); // Trim whitespace
|
|
// Base64 encoding is the most reliable approach for CSS background images
|
return 'url(data:image/svg+xml;base64,' . base64_encode($svg) . ')';
|
}
|
// Convert the SVG to a data URI
|
return 'data:image/svg+xml;base64,' . base64_encode($svg);
|
}
|
|
public function renderCachePage():void
|
{
|
$connections = CacheManager::getAllConnections();
|
|
// Separate generic vs. specific caches
|
$generic_groups = [];
|
$content_specific = [];
|
$nonce = wp_create_nonce('wp_rest');
|
|
foreach ($connections as $group => $configs) {
|
$is_generic = !$this->isBoundToContentOrTaxonomy($group);
|
|
if ($is_generic) {
|
$generic_groups[$group] = $configs;
|
} else {
|
$content_specific[$group] = $configs;
|
}
|
}
|
|
?>
|
<div class="wrap jvb-admin-wrap">
|
<h1>Cache Management</h1>
|
|
<div class="jvb-cache-actions">
|
<button type="button" class="button button-primary" data-action="flush-all">
|
<?= jvbIcon('arrows-clockwise'); ?>
|
Flush All Caches
|
</button>
|
</div>
|
|
<div class="jvb-cache-section">
|
<h2>Generic Caches & Connections</h2>
|
<table class="wp-list-table widefat fixed striped">
|
<thead>
|
<tr>
|
<th class="manage-column">Cache Group</th>
|
<th class="manage-column">Connected To</th>
|
<th class="manage-column">Actions</th>
|
</tr>
|
</thead>
|
<tbody>
|
<?php if (empty($generic_groups)): ?>
|
<tr><td colspan="3">No generic caches registered</td></tr>
|
<?php else: ?>
|
<?php foreach ($generic_groups as $group => $configs): ?>
|
<tr>
|
<td><strong><?= esc_html($group); ?></strong></td>
|
<td><?= $this->formatConnections($configs); ?></td>
|
<td>
|
<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
|
<?= jvbIcon('trash'); ?> Flush
|
</button>
|
</td>
|
</tr>
|
<?php endforeach; ?>
|
<?php endif; ?>
|
</tbody>
|
</table>
|
</div>
|
|
<details class="jvb-cache-section">
|
<summary><h2>Content-Specific Caches</h2></summary>
|
<table class="wp-list-table widefat fixed striped">
|
<thead>
|
<tr>
|
<th>Cache Group</th>
|
<th>Connected To</th>
|
<th>Actions</th>
|
</tr>
|
</thead>
|
<tbody>
|
<?php foreach ($content_specific as $group => $configs): ?>
|
<tr>
|
<td><strong><?= esc_html($group); ?></strong></td>
|
<td><?= $this->formatConnections($configs); ?></td>
|
<td>
|
<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
|
<?= jvbIcon('trash'); ?> Flush
|
</button>
|
</td>
|
</tr>
|
<?php endforeach; ?>
|
</tbody>
|
</table>
|
</details>
|
</div>
|
<script>
|
(function() {
|
const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-cache')); ?>';
|
const nonce = '<?= esc_js($nonce); ?>';
|
|
function callCacheAction(action, data = {}) {
|
const body = { action, ...data };
|
|
return fetch(apiUrl, {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': nonce
|
},
|
body: JSON.stringify(body)
|
})
|
.then(response => response.json())
|
.then(data => {
|
if (data.success) {
|
alert(data.message || 'Success!');
|
location.reload();
|
} else {
|
alert('Error: ' + (data.message || 'Unknown error'));
|
}
|
})
|
.catch(error => {
|
alert('Network error: ' + error.message);
|
console.error('Error:', error);
|
});
|
}
|
|
// Flush all caches
|
document.querySelector('[data-action="flush-all"]')?.addEventListener('click', function() {
|
if (confirm('Flush all caches? This may temporarily slow down your site.')) {
|
this.disabled = true;
|
callCacheAction('flush-all');
|
}
|
});
|
|
// Flush individual cache groups
|
document.querySelectorAll('[data-action="flush-cache"]').forEach(btn => {
|
btn.addEventListener('click', function() {
|
const group = this.getAttribute('data-group');
|
if (confirm(`Flush cache group "${group}"?`)) {
|
this.disabled = true;
|
callCacheAction('flush-cache', { group: group });
|
}
|
});
|
});
|
})();
|
</script>
|
<?php
|
}
|
|
protected function isBoundToContentOrTaxonomy(string $group): bool
|
{
|
$group = jvbNoBase($group);
|
|
if (defined('JVB_CONTENT')) {
|
foreach (JVB_CONTENT as $key => $config) {
|
if (jvbNoBase($key) === $group) {
|
return true;
|
}
|
}
|
}
|
|
if (defined('JVB_TAXONOMY')) {
|
foreach (JVB_TAXONOMY as $key => $config) {
|
if (jvbNoBase($key) === $group) {
|
return true;
|
}
|
}
|
}
|
|
return false;
|
}
|
|
protected function formatConnections(array $configs): string
|
{
|
$connections = [];
|
foreach ($configs as $config) {
|
$parent = $config['parent'] ?? 'unknown';
|
$scope = $config['scope'] ?? 'id';
|
$connections[] = "{$parent} ({$scope})";
|
}
|
return esc_html(implode(', ', $connections));
|
}
|
|
public function handleCacheActions($response, $request, $action):WP_REST_Response
|
{
|
if (!str_starts_with($action, 'flush-')) {
|
return $response;
|
}
|
|
if ($action === 'flush-all') {
|
wp_cache_flush();
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'All caches flushed successfully'
|
]);
|
}
|
|
if (str_starts_with($action, 'flush-cache')) {
|
$group = $request->get_param('group');
|
if (empty($group)) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'No cache group specified'
|
], 400);
|
}
|
|
\JVBase\managers\CacheManager::invalidateAll($group);
|
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => "Cache group '{$group}' flushed successfully"
|
]);
|
}
|
|
if ($action === 'merge-icon-versions') {
|
$timestamps = $request->get_param('timestamps');
|
|
if (empty($timestamps) || !is_array($timestamps)) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'No versions selected for merging'
|
], 400);
|
}
|
|
// Convert to integers
|
$timestamps = array_map('intval', $timestamps);
|
|
if (count($timestamps) < 2) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Please select at least 2 versions to merge'
|
], 400);
|
}
|
|
$icons = \JVBase\managers\IconsManager::getInstance();
|
|
if ($icons->mergeVersions($timestamps)) {
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'Icon versions merged successfully'
|
]);
|
}
|
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Failed to merge icon versions'
|
], 500);
|
}
|
|
if ($action === 'refresh-icons') {
|
$icons = \JVBase\managers\IconsManager::getInstance();
|
$icons->forceRefresh();
|
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'Icon CSS refresh triggered'
|
]);
|
}
|
|
if ($action === 'restore-icon-version') {
|
$timestamp = (int)$request->get_param('timestamp');
|
$icons = \JVBase\managers\IconsManager::getInstance();
|
|
if ($icons->restoreVersion($timestamp)) {
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'Icon version restored successfully'
|
]);
|
}
|
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Failed to restore icon version'
|
], 500);
|
}
|
|
return $response;
|
}
|
|
public function renderIconsPage():void
|
{
|
$icons = \JVBase\managers\IconsManager::getInstance();
|
$versions = $icons->getVersionHistory();
|
$nonce = wp_create_nonce('wp_rest');
|
|
?>
|
<div class="wrap jvb-admin-wrap">
|
<h1>Icon Management</h1>
|
|
<div class="jvb-icon-actions">
|
<button type="button" class="button button-primary" data-action="refresh-icons">
|
<?= jvbIcon('arrows-clockwise'); ?>
|
Force Refresh CSS
|
</button>
|
<button type="button" class="button" data-action="merge-icon-versions" id="merge-versions-btn" disabled>
|
<?= jvbIcon('git-merge'); ?>
|
Merge Selected Versions
|
</button>
|
</div>
|
|
<h2>Version History</h2>
|
<table class="wp-list-table widefat fixed striped">
|
<thead>
|
<tr>
|
<th class="check-column">
|
<input type="checkbox" id="select-all-versions">
|
<label for="select-all-versions" class="screen-reader-text">Select All</label>
|
</th>
|
<th>Date/Time</th>
|
<th>Icon Count</th>
|
<th>File Size</th>
|
<th>Actions</th>
|
</tr>
|
</thead>
|
<tbody>
|
<?php if (empty($versions)): ?>
|
<tr><td colspan="5">No version history available</td></tr>
|
<?php else: ?>
|
<?php foreach (array_reverse($versions) as $index => $version): ?>
|
<tr>
|
<th class="check-column">
|
<input type="checkbox"
|
name="version-select"
|
class="version-checkbox"
|
value="<?= esc_attr($version['timestamp']); ?>">
|
</th>
|
<td><?= esc_html(date('Y-m-d H:i:s', $version['timestamp'])); ?></td>
|
<td>
|
<?= esc_html($version['icon_count']); ?> icons
|
<button type="button"
|
class="button-link"
|
data-action="view-icon-list"
|
data-timestamp="<?= esc_attr($version['timestamp']); ?>">
|
(view)
|
</button>
|
</td>
|
<td><?= esc_html($version['size_formatted']); ?></td>
|
<td>
|
<button type="button" class="button"
|
data-action="restore-icon-version"
|
data-timestamp="<?= esc_attr($version['timestamp']); ?>">
|
<?= jvbIcon('arrow-counter-clockwise'); ?> Restore
|
</button>
|
</td>
|
</tr>
|
<tr id="icon-list-<?= esc_attr($version['timestamp']); ?>" class="icon-list-row" style="display: none;">
|
<td colspan="5">
|
<div class="icon-list-content">
|
<?php foreach ($version['iconList'] as $style => $icons): ?>
|
<strong><?= esc_html(ucfirst($style)); ?>:</strong>
|
<?= esc_html(implode(', ', $icons)); ?><br>
|
<?php endforeach; ?>
|
</div>
|
</td>
|
</tr>
|
<?php endforeach; ?>
|
<?php endif; ?>
|
</tbody>
|
</table>
|
</div>
|
|
<script>
|
(function() {
|
const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
|
const nonce = '<?= esc_js($nonce); ?>';
|
|
// Helper function for API calls
|
function callIconAction(action, data = {}) {
|
const body = { action, ...data };
|
|
return fetch(apiUrl, {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': nonce
|
},
|
body: JSON.stringify(body)
|
})
|
.then(response => response.json())
|
.then(data => {
|
if (data.success) {
|
alert(data.message || 'Success!');
|
location.reload();
|
} else {
|
alert('Error: ' + (data.message || 'Unknown error'));
|
}
|
return data;
|
})
|
.catch(error => {
|
alert('Network error: ' + error.message);
|
console.error('Error:', error);
|
});
|
}
|
|
// Enable/disable merge button based on selection
|
document.querySelectorAll('.version-checkbox').forEach(checkbox => {
|
checkbox.addEventListener('change', function() {
|
const checkedCount = document.querySelectorAll('.version-checkbox:checked').length;
|
document.getElementById('merge-versions-btn').disabled = checkedCount < 2;
|
});
|
});
|
|
// Select all functionality
|
const selectAll = document.getElementById('select-all-versions');
|
if (selectAll) {
|
selectAll.addEventListener('change', function() {
|
document.querySelectorAll('.version-checkbox').forEach(checkbox => {
|
checkbox.checked = this.checked;
|
checkbox.dispatchEvent(new Event('change'));
|
});
|
});
|
}
|
|
// Toggle icon list view
|
document.querySelectorAll('.view-icon-list-btn').forEach(btn => {
|
btn.addEventListener('click', function() {
|
const timestamp = this.getAttribute('data-timestamp');
|
const row = document.getElementById('icon-list-' + timestamp);
|
if (row) {
|
row.style.display = row.style.display === 'none' ? '' : 'none';
|
}
|
});
|
});
|
|
// Force refresh button
|
const refreshBtn = document.getElementById('refresh-icons-btn');
|
if (refreshBtn) {
|
refreshBtn.addEventListener('click', function() {
|
if (confirm('Force regenerate icon CSS? This will reload the page.')) {
|
this.disabled = true;
|
callIconAction('refresh-icons');
|
}
|
});
|
}
|
|
// Merge versions button
|
const mergeBtn = document.getElementById('merge-versions-btn');
|
if (mergeBtn) {
|
mergeBtn.addEventListener('click', function() {
|
const checkboxes = document.querySelectorAll('.version-checkbox:checked');
|
const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
|
if (timestamps.length < 2) {
|
alert('Please select at least 2 versions to merge');
|
return;
|
}
|
|
if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
|
this.disabled = true;
|
callIconAction('merge-icon-versions', { timestamps: timestamps });
|
}
|
});
|
}
|
|
// Restore version buttons
|
document.querySelectorAll('.restore-version-btn').forEach(btn => {
|
btn.addEventListener('click', function() {
|
const timestamp = parseInt(this.getAttribute('data-timestamp'));
|
|
if (confirm('Restore this icon version? This will reload the page.')) {
|
this.disabled = true;
|
callIconAction('restore-icon-version', { timestamp: timestamp });
|
}
|
});
|
});
|
})();
|
</script>
|
<?php
|
}
|
}
|