Jake Vanderwerf
8 days ago 3b83905603d44b1a08f8b2b36a605808ce686ad6
inc/managers/AdminPages.php
@@ -1,7 +1,8 @@
<?php
namespace JVBase\managers;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use WP_REST_Response;
if (!defined('ABSPATH')) {
@@ -31,7 +32,7 @@
            'menu_title' => 'JakeVan',
            'capability' => 'manage_options',
            'menu_slug' => BASE . 'settings',
            'icon' => jvbCSSIcon('settings'),
            'icon' => jvbCSSIcon('gear-six'),
            'position' => 0
        ];
      $this->subpages = get_option(BASE.'adminSubpage', []);
@@ -47,164 +48,11 @@
      add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
      add_action('rest_api_init', [$this, 'registerRestRoutes']);
      // Handle form submissions
      add_action('admin_init', [$this, 'handleAdminPageSubmission']);
      add_action('admin_notices', [$this, 'displayAdminNotices']);
    }
   /**
    * 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'));
      $source = sanitize_text_field($request->get_param('source') ?? 'icons'); // Add source param
      $icons = \JVBase\managers\IconsManager::for($source);
      switch ($action) {
         case 'refresh-icons':
            $icons->forceRefresh();
            return new \WP_REST_Response([
               'success' => true,
               'message' => "Icon CSS regenerated successfully for '{$source}'"
            ]);
         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
     *
@@ -370,6 +218,11 @@
        }
    }
   public function getMainConfig():array
   {
      return $this->main_page;
   }
    /**
     * Render the main settings page
     */
@@ -465,11 +318,11 @@
        </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
    }
@@ -501,10 +354,8 @@
        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">
@@ -597,40 +448,41 @@
     *
     * @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
@@ -659,20 +511,17 @@
   public function renderCachePage():void
   {
      $connections = CacheManager::getAllConnections();
      $groups = Cache::getAllGroups();
      // Separate generic vs. specific caches
      $generic_groups = [];
      $content_specific = [];
      $nonce = wp_create_nonce('wp_rest');
      // Separate by type
      $generic = [];
      $specific = [];
      foreach ($connections as $group => $configs) {
         $is_generic = !$this->isBoundToContentOrTaxonomy($group);
         if ($is_generic) {
            $generic_groups[$group] = $configs;
      foreach ($groups as $group => $data) {
         if ($this->isBoundToContentOrTaxonomy($group)) {
            $specific[$group] = $data;
         } else {
            $content_specific[$group] = $configs;
            $generic[$group] = $data;
         }
      }
@@ -681,7 +530,9 @@
         <h1>Cache Management</h1>
         <div class="jvb-cache-actions">
            <button type="button" class="button button-primary" data-action="flush-all">
            <button type="button"
                  class="button button-primary"
                  data-cache-action="flush-all">
               <?= jvbDashIcon('arrows-clockwise'); ?>
               Flush All Caches
            </button>
@@ -698,15 +549,18 @@
               </tr>
               </thead>
               <tbody>
               <?php if (empty($generic_groups)): ?>
               <?php if (empty($generic)): ?>
                  <tr><td colspan="3">No generic caches registered</td></tr>
               <?php else: ?>
                  <?php foreach ($generic_groups as $group => $configs): ?>
                  <?php foreach ($generic as $group => $data): ?>
                     <tr>
                        <td><strong><?= esc_html($group); ?></strong></td>
                        <td><?= $this->formatConnections($configs); ?></td>
                        <td><?= $this->formatConnections($data); ?></td>
                        <td>
                           <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
                           <button type="button"
                                 class="button"
                                 data-cache-action="flush-cache"
                                 data-group="<?= esc_attr($group); ?>">
                              <?= jvbDashIcon('trash'); ?> Flush
                           </button>
                        </td>
@@ -728,12 +582,15 @@
               </tr>
               </thead>
               <tbody>
               <?php foreach ($content_specific as $group => $configs): ?>
               <?php foreach ($specific as $group => $data): ?>
                  <tr>
                     <td><strong><?= esc_html($group); ?></strong></td>
                     <td><?= $this->formatConnections($configs); ?></td>
                     <td><?= $this->formatConnections($data); ?></td>
                     <td>
                        <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
                        <button type="button"
                              class="button"
                              data-cache-action="flush-cache"
                              data-group="<?= esc_attr($group); ?>">
                           <?= jvbDashIcon('trash'); ?> Flush
                        </button>
                     </td>
@@ -743,57 +600,6 @@
            </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
   }
@@ -801,34 +607,33 @@
   {
      $group = jvbNoBase($group);
      if (defined('JVB_CONTENT')) {
         foreach (JVB_CONTENT as $key => $config) {
            if (jvbNoBase($key) === $group) {
               return true;
            }
      $registered = Registrar::getRegistered();
      foreach ($registered as $r) {
         if ($r === $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
   protected function formatConnections(array $data): string
   {
      $connections = [];
      foreach ($configs as $config) {
         $parent = $config['parent'] ?? 'unknown';
         $scope = $config['scope'] ?? 'id';
         $connections[] = "{$parent} ({$scope})";
      $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);
      }
      return esc_html(implode(', ', $connections));
      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
@@ -854,7 +659,8 @@
            ], 400);
         }
         \JVBase\managers\CacheManager::invalidateAll($group);
         $group = sanitize_text_field($request->get_param('group'));
         Cache::invalidateGroup($group);
         return new WP_REST_Response([
            'success' => true,
@@ -862,68 +668,6 @@
         ]);
      }
      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;
   }
@@ -934,11 +678,10 @@
      $current_source = sanitize_text_field($current_source);
      // Get all registered icon sources
      $all_sources = ['icons', 'forms', 'dash']; // You could get this dynamically if needed
      $all_sources = ['icons', 'forms', 'dash'];
      $icons = \JVBase\managers\IconsManager::for($current_source);
      $icons = IconsManager::for($current_source);
      $versions = $icons->getVersionHistory();
      $nonce = wp_create_nonce('wp_rest');
      ?>
      <div class="wrap jvb-admin-wrap">
@@ -947,9 +690,11 @@
         <!-- 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">
            <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); ?>>
                  <option value="<?= esc_attr($source); ?>"
                     <?= selected($current_source, $source, false); ?>>
                     <?= esc_html(ucfirst($source)); ?>
                  </option>
               <?php endforeach; ?>
@@ -957,11 +702,19 @@
         </div>
         <div class="jvb-icon-actions">
            <button type="button" class="button button-primary" data-action="refresh-icons" data-source="<?= esc_attr($current_source); ?>">
            <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-action="merge-icon-versions" data-source="<?= esc_attr($current_source); ?>" id="merge-versions-btn" disabled>
            <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>
@@ -1004,15 +757,18 @@
                     </td>
                     <td><?= esc_html($version['size_formatted']); ?></td>
                     <td>
                        <button type="button" class="button restore-version-btn"
                              data-action="restore-icon-version"
                        <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;">
                  <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): ?>
@@ -1027,108 +783,6 @@
            </tbody>
         </table>
      </div>
      <script>
         (function() {
            const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
            const nonce = '<?= esc_js($nonce); ?>';
            const currentSource = '<?= esc_js($current_source); ?>';
            // Helper function for API calls
            function callIconAction(action, data = {}) {
               const body = { action, source: currentSource, ...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
            document.querySelector('[data-action="refresh-icons"]')?.addEventListener('click', function() {
               if (confirm('Force regenerate icon CSS? This will reload the page.')) {
                  this.disabled = true;
                  callIconAction('refresh-icons');
               }
            });
            // Merge versions button
            document.getElementById('merge-versions-btn')?.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
   }