| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_REST_Response; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | 'menu_title' => 'JakeVan', |
| | | 'capability' => 'manage_options', |
| | | 'menu_slug' => BASE . 'settings', |
| | | 'icon' => jvbCSSIcon('settings'), |
| | | 'icon' => jvbCSSIcon('gear-six'), |
| | | 'position' => 0 |
| | | ]; |
| | | $this->subpages = apply_filters('jvbAdminSubpages', []); |
| | | $this->subpages = get_option(BASE.'adminSubpage', []); |
| | | // delete_option(BASE.'admin_actions'); |
| | | // delete_option(BASE.'admin_subpages'); |
| | | // $this->getSubpages(); |
| | |
| | | // 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); |
| | | |
| | | // Handle form submissions |
| | | add_action('admin_init', [$this, 'handleAdminPageSubmission']); |
| | | add_action('admin_notices', [$this, 'displayAdminNotices']); |
| | | } |
| | | |
| | | /** |
| | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | public function getMainConfig():array |
| | | { |
| | | return $this->main_page; |
| | | } |
| | | |
| | | /** |
| | | * Render the main settings page |
| | | */ |
| | |
| | | </div> |
| | | </div> |
| | | <?php |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | protected function renderStatusItems():void |
| | | { |
| | | // Get queue stats |
| | | $queue_status = JVB()->queue()->getQueueStatus(); |
| | | $queue_status = JVB()->queue()->getStatus(); |
| | | error_log('Queue Status: '.print_r($queue_status, true)); |
| | | |
| | | // Other system checks |
| | |
| | | </li> |
| | | <li> |
| | | <span class="status-label">Content Types:</span> |
| | | <span class="status-value"><?= count(JVB_CONTENT); ?> registered</span> |
| | | <span class="status-value"><?= count(Registrar::getRegistered('post')); ?> registered</span> |
| | | </li> |
| | | <li> |
| | | <span class="status-label">Taxonomies:</span> |
| | | <span class="status-value"><?= count(JVB_TAXONOMY); ?> registered</span> |
| | | <span class="status-value"><?= count(Registrar::getRegistered('term')); ?> registered</span> |
| | | </li> |
| | | <?php |
| | | } |
| | |
| | | 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']; |
| | | } |
| | | $content_types = array_map(function ($type) { return jvbCheckBase($type); }, |
| | | Registrar::getRegistered('post')); |
| | | |
| | | ?> |
| | | <table class="jvb-content-table"> |
| | |
| | | if (current_user_can($action['capability'])) { |
| | | ?> |
| | | <a data-action="<?=$action['slug']?>" class="jvb-action"> |
| | | <?= jvbIcon($action['icon']); ?> |
| | | <?= jvbDashIcon($action['icon']); ?> |
| | | <span class="jvb-link-title"><?= esc_html($action['label'])?></span> |
| | | <span class="loader"><?=jvbIcon('refresh')?><?=jvbIcon('check')?></span> |
| | | <span class="loader"><?=jvbDashIcon('arrows-clockwise')?><?=jvbDashIcon('check')?></span> |
| | | </a> |
| | | <?php |
| | | } |
| | |
| | | * |
| | | * @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; |
| | | } |
| | | public function enqueueAdminAssets(string $hook):void |
| | | { |
| | | // More robust check for JVB admin pages |
| | | 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 styles |
| | | wp_enqueue_style( |
| | | 'jvb-admin-styles', |
| | | JVB_URL . 'assets/css/admin.css', |
| | | [], |
| | | '1.1' |
| | | ); |
| | | |
| | | // Enqueue admin scripts |
| | | wp_enqueue_script( |
| | | 'jvb-admin-scripts', |
| | | JVB_URL . 'assets/js/admin.js', |
| | | [], |
| | | '1.0.0', |
| | | true |
| | | ); |
| | | // Enqueue admin scripts - make sure jvb-auth is loaded first |
| | | wp_enqueue_script( |
| | | 'jvb-admin-scripts', |
| | | JVB_URL . 'assets/js/admin.js', |
| | | ['jvb-auth'], |
| | | '1.1', |
| | | ['strategy' => 'defer', 'in_footer' => 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'), |
| | | ] |
| | | ); |
| | | } |
| | | // Localize to jvb-admin-scripts as well for redundancy |
| | | wp_localize_script( |
| | | 'jvb-auth', |
| | | 'jvbSettings', |
| | | [ |
| | | 'api' => rest_url('jvb/v1/'), |
| | | 'nonce' => wp_create_nonce('wp_rest'), |
| | | 'action' => wp_create_nonce('itsme'), |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Create a custom SVG icon for the admin menu |
| | |
| | | */ |
| | | protected function getIcon(string $icon = 'logo', bool $css = false): string |
| | | { |
| | | $svg = jvbIcon($icon, ['wrap' => false]); |
| | | $svg = jvbDashIcon($icon, ['wrap' => false]); |
| | | if ($css) { |
| | | // For CSS, replace currentColor with brand color |
| | | $svg = str_replace('currentColor', '#FF0080', $svg); |
| | |
| | | return 'data:image/svg+xml;base64,' . base64_encode($svg); |
| | | } |
| | | |
| | | public function renderCachePage() |
| | | { |
| | | $groups = get_option(BASE.'all_cache_groups', []); |
| | | public function renderCachePage():void |
| | | { |
| | | $groups = Cache::getAllGroups(); |
| | | |
| | | ?> |
| | | <h1>Manage Cache</h1> |
| | | <?php |
| | | foreach ($groups as $group => $caches) { |
| | | ?> |
| | | <details> |
| | | <summary class="row btw"><h2><?=$group?></h2></summary> |
| | | <table> |
| | | <thead> |
| | | <tr> |
| | | <th scope="col"><input type="checkbox" name="select-all-<?=$group?>" id="select-all-<?=$group?>"> |
| | | <label for="select-all-<?=$group?>">All</label></th> |
| | | <th scope="col">Cache Key</th> |
| | | <th scope="col">Actions</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php |
| | | foreach ($caches as $key) { |
| | | ?> |
| | | <tr> |
| | | <td><input type="checkbox" name="select-<?=$group?>-<?=$key?>" id="select-<?=$group?>-<?=$key?>"><label for="select-<?=$group?>-<?=$key?>"></label></td> |
| | | <td><?= $key ?></td> |
| | | <td><button type="button" data-action="flush-<?=$group?>-<?=$key?>"><?= jvbIcon('trash')?></button></td> |
| | | </tr> |
| | | <?php |
| | | } |
| | | ?> |
| | | </tbody> |
| | | </table> |
| | | </details> |
| | | <?php |
| | | } |
| | | } |
| | | // Separate by type |
| | | $generic = []; |
| | | $specific = []; |
| | | |
| | | foreach ($groups as $group => $data) { |
| | | if ($this->isBoundToContentOrTaxonomy($group)) { |
| | | $specific[$group] = $data; |
| | | } else { |
| | | $generic[$group] = $data; |
| | | } |
| | | } |
| | | |
| | | ?> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Cache Management</h1> |
| | | |
| | | <div class="jvb-cache-actions"> |
| | | <button type="button" |
| | | class="button button-primary" |
| | | data-cache-action="flush-all"> |
| | | <?= jvbDashIcon('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)): ?> |
| | | <tr><td colspan="3">No generic caches registered</td></tr> |
| | | <?php else: ?> |
| | | <?php foreach ($generic as $group => $data): ?> |
| | | <tr> |
| | | <td><strong><?= esc_html($group); ?></strong></td> |
| | | <td><?= $this->formatConnections($data); ?></td> |
| | | <td> |
| | | <button type="button" |
| | | class="button" |
| | | data-cache-action="flush-cache" |
| | | data-group="<?= esc_attr($group); ?>"> |
| | | <?= jvbDashIcon('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 ($specific as $group => $data): ?> |
| | | <tr> |
| | | <td><strong><?= esc_html($group); ?></strong></td> |
| | | <td><?= $this->formatConnections($data); ?></td> |
| | | <td> |
| | | <button type="button" |
| | | class="button" |
| | | data-cache-action="flush-cache" |
| | | data-group="<?= esc_attr($group); ?>"> |
| | | <?= jvbDashIcon('trash'); ?> Flush |
| | | </button> |
| | | </td> |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | </tbody> |
| | | </table> |
| | | </details> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | protected function isBoundToContentOrTaxonomy(string $group): bool |
| | | { |
| | | $group = jvbNoBase($group); |
| | | |
| | | $registered = Registrar::getRegistered(); |
| | | foreach ($registered as $r) { |
| | | if ($r === $group) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | protected function formatConnections(array $data): string |
| | | { |
| | | $parts = []; |
| | | |
| | | if (!empty($data['connects_to'])) { |
| | | $targets = array_map(function($conn) { |
| | | $flush_text = $conn['flush'] ? ' (flush all)' : ''; |
| | | return $conn['group'] . $flush_text; |
| | | }, $data['connects_to']); |
| | | $parts[] = '<strong>Invalidates:</strong> ' . implode(', ', $targets); |
| | | } |
| | | |
| | | if (!empty($data['connected_from'])) { |
| | | $sources = array_map(fn($conn) => $conn['group'], $data['connected_from']); |
| | | $parts[] = '<strong>Invalidated by:</strong> ' . implode(', ', $sources); |
| | | } |
| | | |
| | | return $parts ? implode('<br>', $parts) : 'No 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); |
| | | } |
| | | |
| | | $group = sanitize_text_field($request->get_param('group')); |
| | | Cache::invalidateGroup($group); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => "Cache group '{$group}' flushed successfully" |
| | | ]); |
| | | } |
| | | |
| | | return $response; |
| | | } |
| | | |
| | | public function renderIconsPage():void |
| | | { |
| | | // Get current source from query param or default to 'icons' |
| | | $current_source = $_GET['icon_source'] ?? 'icons'; |
| | | $current_source = sanitize_text_field($current_source); |
| | | |
| | | // Get all registered icon sources |
| | | $all_sources = ['icons', 'forms', 'dash']; |
| | | |
| | | $icons = IconsManager::for($current_source); |
| | | $versions = $icons->getVersionHistory(); |
| | | |
| | | ?> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Icon Management</h1> |
| | | |
| | | <!-- Source Selector --> |
| | | <div class="jvb-icon-source-selector"> |
| | | <label for="icon-source-select">Icon Source:</label> |
| | | <select id="icon-source-select" |
| | | onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value"> |
| | | <?php foreach ($all_sources as $source): ?> |
| | | <option value="<?= esc_attr($source); ?>" |
| | | <?= selected($current_source, $source, false); ?>> |
| | | <?= esc_html(ucfirst($source)); ?> |
| | | </option> |
| | | <?php endforeach; ?> |
| | | </select> |
| | | </div> |
| | | |
| | | <div class="jvb-icon-actions"> |
| | | <button type="button" |
| | | class="button button-primary" |
| | | data-icon-action="refresh-icons" |
| | | data-source="<?= esc_attr($current_source); ?>"> |
| | | <?= jvbDashIcon('arrows-clockwise'); ?> |
| | | Force Refresh CSS |
| | | </button> |
| | | <button type="button" |
| | | class="button" |
| | | data-icon-action="merge-icon-versions" |
| | | data-source="<?= esc_attr($current_source); ?>" |
| | | id="merge-versions-btn" |
| | | disabled> |
| | | <?= jvbDashIcon('git-merge'); ?> |
| | | Merge Selected Versions |
| | | </button> |
| | | </div> |
| | | |
| | | <h2>Version History for <?= esc_html(ucfirst($current_source)); ?></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 view-icon-list-btn" |
| | | data-timestamp="<?= esc_attr($version['timestamp']); ?>"> |
| | | (view) |
| | | </button> |
| | | </td> |
| | | <td><?= esc_html($version['size_formatted']); ?></td> |
| | | <td> |
| | | <button type="button" |
| | | class="button restore-version-btn" |
| | | data-icon-action="restore-icon-version" |
| | | data-source="<?= esc_attr($current_source); ?>" |
| | | data-timestamp="<?= esc_attr($version['timestamp']); ?>"> |
| | | <?= jvbDashIcon('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> |
| | | <?php |
| | | } |
| | | |
| | | public static function addSubpage(string $key, array $value):void |
| | | { |
| | | $option = get_option(BASE.'adminSubpage', []); |
| | | if (empty($option) || !array_key_exists($key, $option)) { |
| | | $option[$key] = $value; |
| | | update_option(BASE.'adminSubpage', $option); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle admin page form submissions |
| | | * Fires after WordPress admin_init |
| | | */ |
| | | public function handleAdminPageSubmission(): void |
| | | { |
| | | // Only process on our admin pages |
| | | if (!isset($_GET['page']) || strpos($_GET['page'], BASE) !== 0) { |
| | | return; |
| | | } |
| | | |
| | | // Check for form submission |
| | | if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['submit'])) { |
| | | return; |
| | | } |
| | | |
| | | // Verify nonce |
| | | $nonce_field = BASE . 'admin_page_nonce'; |
| | | if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], $nonce_field)) { |
| | | add_action('admin_notices', function() { |
| | | echo '<div class="notice notice-error"><p>Security check failed. Please try again.</p></div>'; |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | $page_slug = sanitize_text_field($_GET['page']); |
| | | |
| | | // Allow other classes to handle their page submissions |
| | | $result = apply_filters('jvb_admin_page_submission', null, $page_slug, $_POST); |
| | | |
| | | // Store result in transient for after redirect |
| | | if (is_array($result)) { |
| | | set_transient(BASE . 'admin_notice_' . get_current_user_id(), $result, 30); |
| | | } |
| | | |
| | | // Redirect to prevent form resubmission (POST-Redirect-GET pattern) |
| | | wp_safe_redirect(add_query_arg(['page' => $page_slug], admin_url('admin.php'))); |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Display admin notices from form submissions |
| | | */ |
| | | public function displayAdminNotices(): void |
| | | { |
| | | $notice = get_transient(BASE . 'admin_notice_' . get_current_user_id()); |
| | | |
| | | if ($notice && is_array($notice)) { |
| | | delete_transient(BASE . 'admin_notice_' . get_current_user_id()); |
| | | |
| | | $type = $notice['success'] ? 'success' : 'error'; |
| | | $message = $notice['message'] ?? 'Settings saved.'; |
| | | |
| | | echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>'; |
| | | } |
| | | } |
| | | } |