Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
inc/rest/routes/ContentRoutes.php
@@ -25,14 +25,18 @@
    protected string $post_type = '';
    protected string $user_id = '';
   //For Timeline-specific posts
   protected array $timelineSharedFields = [];
   protected array $timelineUniqueFields = [];
    //TODO: Ensure we are handling the bulk operations for all processes
    //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create
    //TODO: Also invalidate feed caches on updates!!
    public function __construct()
    {
        $this->cache_name = 'user_content_'.get_current_user_id();
        parent::__construct();
        $this->action = 'dash-';
        $this->operation_type = 'content_update';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -75,6 +79,32 @@
        ]);
    }
   protected function initTimelineFields(string $content):void
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
         return;
      }
      $config = Features::getConfig($content);
      $this->fields = $config['fields'];
      $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
            return true;
         }
         return false;
      }));
      array_unshift($this->timelineSharedFields, 'post_thumbnail');
      array_unshift($this->timelineSharedFields, 'post_title');
      $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
         if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
            return true;
         }
         return false;
      }));
   }
    /**
     * Handle content update/creation
     * @param WP_REST_Request $request
@@ -187,9 +217,6 @@
        }
        $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
      $config = Features::getConfig($params['content']);
        // Build query args
        $args = [
@@ -396,14 +423,12 @@
         }
            CacheManager::invalidateGroup($post_data['content']);
            CacheManager::for($post_data['content'])->clear();
         if (jvbSiteUsesFeedBlock()) {
            CacheManager::invalidateGroup($post_data['feed']);
            CacheManager::for('feed')->clear();
         }
        }
        CacheManager::invalidateGroup('user_content');
      if (jvbSiteHasNotifications()) {
         $this->notifications = JVB()->notification();
         $this->notifications->addNotification(
@@ -433,83 +458,109 @@
         return ['success' => false, 'message' => 'No permission'];
      }
      $rows = $post_data['fields'] ?? [];
      if (empty($rows)) {
         return ['success' => false, 'message' => 'No data'];
      }
      $this->fields = jvbGetFields($post_data['content']);
      $this->initTimelineFields($post_data['content']);
      error_log('Received Data: '.print_r($post_data, true));
      $fields = jvbGetFields($post_data['content']);
      // First row = parent post
      $parent_row = array_shift($rows);
      if (($parent_row['id'] ?? null) != $parent_id) {
         return ['success' => false, 'message' => 'Parent ID mismatch'];
      }
      $allowedFields = array_filter($parent_row, function($key) use ($fields) {
         return array_key_exists($key, $fields);
      // First, process the main fields that will apply to all posts
      $sharedData = array_filter($post_data, function ($key) {
         return in_array($key, $this->timelineSharedFields);
      }, ARRAY_FILTER_USE_KEY);
      $parentMeta = new MetaManager($parent_id, 'post');
      $parentMeta->setAll($allowedFields);
      //Next, process any individual posts, including any menu order changes
      if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
         $sharedTaxonomies = $sharedData;
         unset($sharedTaxonomies['post_title']);
      // Get existing children to track deletions
      $existing_children = get_children([
         'post_parent' => $parent_id,
         'post_type' => jvbCheckBase($post_data['content']),
         'fields' => 'ids'
      ]);
      $processed_ids = [];
      // Process remaining rows as children
      foreach ($rows as $index => $row_data) {
         $row_id = $row_data['id'] ?? null;
         // New child post
         if (!$row_id || str_starts_with($row_id, 'new')) {
            $child_id = wp_insert_post([
               'post_type' => jvbCheckBase($post_data['content']),
               'post_parent' => $parent_id,
               'post_author' => $this->user_id,
               'post_status' => $post_data['status'] ?? 'draft',
               'menu_order' => $index
            ]);
         //Ensure the parent post exists and is still first in the array
         $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
         error_log('Found index: '.print_r($index, true));
         if ($index === false) {
            return [
               'success' => false,
               'message'   => 'Missing parent id. This should not have happened'
            ];
         } elseif ($index !== 0) {
            // Move that element to the start of the array
            $item = $post_data['timeline'][$index];
            unset($post_data['timeline'][$index]);
            array_unshift($post_data['timeline'], $item);
         }
         // Existing child post
         else {
            $child_id = (int) $row_id;
         $errors = [];
         $success = [];
         // Get existing children to track deletions
         $existing_children = get_children([
            'post_parent' => $parent_id,
            'post_type' => jvbCheckBase($post_data['content']),
            'fields' => 'ids'
         ]);
         //Iterate through the timeline posts
         foreach($post_data['timeline'] as $order => $timeline) {
            $allowedFields = array_filter($timeline, function($key) {
               return in_array($key, $this->timelineUniqueFields);
            }, ARRAY_FILTER_USE_KEY);
            // Verify ownership via parent
            if (!in_array($child_id, $existing_children)) {
               continue; // Skip if not actually a child of this parent
            $allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
            $allowedFields = array_merge($allowedFields, $sharedTaxonomies);
            if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
               $newChild = wp_insert_post([
                  'post_author'  => $this->user_id,
                  'post_type'    => jvbCheckBase($post_data['content']),
                  'post_title'   => $allowedFields['post_title'],
                  'post_parent'  => $parent_id,
                  'menu_order'   => $order
               ]);
               if (!$newChild || is_wp_error($newChild)) {
                  $errors[] = [
                     'message'   => 'Could not create child post',
                     'data'      => $timeline
                  ];
                  continue;
               }
               $timeline['id'] = $newChild;
            }
            // Update menu_order (position may have changed)
            wp_update_post([
               'ID' => $child_id,
               'menu_order' => $index
            ]);
            if (in_array((int)$timeline['id'], $existing_children)) {
               unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
            }
            //Determine which fields to update
            $meta = new MetaManager($timeline['id'], 'post');
            $oldValues = $meta->getAll(array_keys($allowedFields));
            $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
               return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
            }, ARRAY_FILTER_USE_BOTH);
            $meta->setAll($updateValues);
            //Update Menu Order, if applicable
            if ((int) $timeline['id'] !== $parent_id) {
               $post = get_post((int) $timeline['id']);
               if ($post && $post->menu_order !== $order) {
                  $updated = wp_update_post([
                     'ID'        => $post->ID,
                     'menu_order'   => $order,
                  ]);
                  if (!$updated || is_wp_error($updated)) {
                     $errors[] = [
                        'message'   => 'Could not update timeline order for post',
                        'data'      => $timeline
                     ];
                  }
               }
            }
            $success[] = $timeline['id'];
         }
         // Update child meta
         $allowedChildFields = array_filter($row_data, function($key) use ($fields) {
            return array_key_exists($key, $fields) && $key !== 'id' && $key !== 'draggable';
         }, ARRAY_FILTER_USE_KEY);
         $childMeta = new MetaManager($child_id, 'post');
         $childMeta->setAll($allowedChildFields);
         $processed_ids[] = $child_id;
      }
      //Delete any remaining children that no longer exist
      if (!empty($existing_children)) {
         foreach ($existing_children as $ID) {
            wp_delete_post($ID);
         }
      }
      // Delete removed children
      $deleted_ids = array_diff($existing_children, $processed_ids);
      foreach ($deleted_ids as $delete_id) {
         wp_delete_post($delete_id, true);
      }
      return ['success' => true, 'processed' => $processed_ids];
      return ['success' => true, 'data' => [
         'success'   => $success,
         'errors' => $errors
      ]];
   }
    /**
@@ -590,9 +641,10 @@
     *
     * @return array
     */
    protected function prepareItem(WP_Post $post, $skip = false):array
    protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
    {
      if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
         $this->initTimelineFields($post->post_type);
         return $this->formatTimeline($post);
      }
        $this->meta = new MetaManager($post->ID, 'post');
@@ -605,7 +657,7 @@
            'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
            'icon'      => $this->post_type,
            'taxonomies'=> [],
            'fields'    => $this->meta->getAll(),
            'fields'    => ($fields) ? $this->meta->getAll() : [],
         'images' => [],
        ];
@@ -633,12 +685,13 @@
        return $data;
    }
   protected function extractImages():array
   protected function extractImages(array $fields = []):array
   {
      //Extract images
      $images = [];
      $get = [];
      foreach ($this->fields as $field => $config) {
      $fields = (empty($fields)) ? $this->fields : $fields;
      foreach ($fields as $field => $config) {
         if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
            $get[] = $field;
         }
@@ -660,36 +713,29 @@
   protected function formatTimeline(WP_Post $post):array
   {
      $data = $this->prepareItem($post, true);
      $firstRow = $data['fields'];
      $firstRow['id'] = $post->ID;
      $firstRow['draggable'] = false;
      $fields = [$firstRow];
      $item = $this->prepareItem($post, true, false);
      //Step 1: Get the fields that apply to all posts
      $mainMeta = new MetaManager($post->ID, 'post');
      $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order']);
      $allImages = [];
      //Step 2: Get the fields for each individual posts
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
      array_unshift($children, $post->ID);
      $subFields = [];
      $images = [];
      foreach ($children as $child) {
         $this->meta = new MetaManager($child->ID, 'post');
         $row = $this->meta->getAll();  // Store in variable first
         $row['id'] = $child->ID;       // Add ID to the row
         $row['draggable'] = true;      // Mark as draggable
         $fields[] = $row;              // Then append to fields
         $meta = new MetaManager($child, 'post');
         $f = $meta->getAll($this->timelineUniqueFields);
         $f =  ['id' => $child] + $f;
         $subFields[$f['post_thumbnail']] = $f;
         $images = $this->extractImages();
         if (!empty($images)) {
            $allImages = $allImages + $images;
         }
         $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
      }
      $item['fields']['timeline'] = $subFields;
      $item['images'] = $item['images'] + $images;
      if (!empty($allImages)) {
         if (!array_key_exists('images', $data)) {
            $data['images'] = [];
         }
         $data['images'] = $data['images'] + $allImages;
      }
      $data['fields']['timeline'] = $fields;
      return $data;
      return $item;
   }
    /**
@@ -839,9 +885,8 @@
                    }
                    //Clear cache
                    CacheManager::invalidateGroup($data['content']);
                    CacheManager::invalidateGroup('feed');
                    CacheManager::invalidateGroup('user_content');
               CacheManager::for($data['content'])->clear();
                    CacheManager::for('feed')->clear();
                }
            return [