From 58e8ae0759ccfa97c478ccae4e0778bdce70966f Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 22 Jan 2026 22:40:02 +0000
Subject: [PATCH] =DirectoryManager.php updates, some javascript tweaks for CRUD.js, and minor style changes
---
inc/managers/queue/executors/ContentExecutor.php | 452 ++++++++++++++++++++++++++++++--------------------------
1 files changed, 243 insertions(+), 209 deletions(-)
diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index c53f4a7..cf3936d 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -83,22 +83,12 @@
$results = [];
$errors = [];
+ $updateTimelineOrder = [];
+
foreach ($posts as $id => $postData) {
try {
$content = $postData['content'] ?? '';
- // Timeline posts
- if (Features::forContent($content)->has('is_timeline') && isset($postData['timeline'])) {
- $parentId = (int)$id;
- if ($parentId === 0) {
- $progress->failItem($id, 'Invalid parent post ID for timeline');
- continue;
- }
- $results[$id] = $this->processTimelinePost($parentId, $postData);
- $progress->advance(1);
- continue;
- }
-
// New post creation
if (str_starts_with((string)$id, 'new')) {
$newId = wp_insert_post([
@@ -115,6 +105,11 @@
$this->savePostFields($newId, $postData);
$results[$id] = ['success' => true, 'new_id' => $newId];
+
+ if (Features::forContent($content)->has('is_timeline')) {
+ $this->updateTimelineLatestDate($newId);
+ }
+
$progress->advance(1);
continue;
}
@@ -124,8 +119,43 @@
$progress->failItem($id, 'No permission to modify this post');
continue;
}
+ // Check if this is a timeline post
+ $isTimeline = Features::forContent($content)->has('is_timeline');
+ if ($isTimeline) {
+ $post = get_post((int)$id);
+ $parentId = $post->post_parent;
+ $isParent = ($parentId === 0);
+
+ // Track timeline reordering only if date changed
+ if (array_key_exists('post_date', $postData)) {
+ $timelineRoot = $isParent ? (int)$id : $parentId;
+ if (!in_array($timelineRoot, $updateTimelineOrder)) {
+ $updateTimelineOrder[] = $timelineRoot;
+ }
+ }
+
+ // Update shared fields if this is the parent
+ if ($isParent) {
+ $this->initTimelineFields($content);
+ $sharedFieldsUpdated = array_filter($postData, function($key) {
+ return in_array($key, $this->timelineSharedFields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ if (!empty($sharedFieldsUpdated)) {
+ $this->updateSharedFields((int)$id, $sharedFieldsUpdated);
+ }
+ }
+ }
$this->processPostUpdate((int)$id, $postData);
+
+ if (Features::forContent($content)->has('is_timeline')
+ && array_key_exists('post_date', $postData)) {
+ $post = get_post((int)$id);
+ $parentId = $post->post_parent === 0 ? (int)$id : $post->post_parent;
+ $this->updateTimelineLatestDate($parentId);
+ }
+
$results[$id] = ['success' => true];
$progress->advance(1);
@@ -140,6 +170,11 @@
$errors[$id] = $e->getMessage();
}
}
+ if (!empty($updateTimelineOrder)) {
+ foreach ($updateTimelineOrder as $parentID) {
+ $this->reorderTimelineByDate($parentID);
+ }
+ }
// Send notification
if (jvbSiteHasNotifications()) {
@@ -177,6 +212,7 @@
break;
case 'draft':
wp_update_post(['ID' => $postId, 'post_status' => 'draft']);
+ unset($postData['post_status']);
break;
case 'trash':
wp_trash_post($postId);
@@ -187,9 +223,201 @@
}
}
+ // Save all fields via MetaManager (handles post fields too)
$this->savePostFields($postId, $postData);
}
+ private function updateSharedFields(int $parentId, array $sharedFields): void
+ {
+ // Get all posts in timeline
+ $children = get_posts([
+ 'post_type' => get_post_type($parentId),
+ 'post_parent' => $parentId,
+ 'post_status' => ['publish', 'draft'],
+ 'posts_per_page' => -1,
+ 'fields' => 'ids'
+ ]);
+
+ $allPostIds = array_merge([$parentId], $children);
+
+ // Apply shared fields to all posts
+ foreach ($allPostIds as $timelinePostId) {
+ $meta = new MetaManager($timelinePostId, 'post');
+ $meta->setAll($sharedFields);
+ }
+ }
+
+ private function reorderTimelineByDate(int $parentId): void
+ {
+ $parent = get_post($parentId);
+ if (!$parent) return;
+
+ // Get all posts in this timeline (parent + children)
+ $children = get_posts([
+ 'post_type' => get_post_type($parentId),
+ 'post_parent' => $parentId,
+ 'post_status' => ['publish', 'draft'],
+ 'posts_per_page' => -1,
+ 'orderby' => 'date',
+ 'order' => 'ASC'
+ ]);
+
+ // Combine and sort by post_date
+ $allPosts = array_merge([$parent], $children);
+ usort($allPosts, function($a, $b) {
+ return strtotime($a->post_date) <=> strtotime($b->post_date);
+ });
+
+ $newParent = $allPosts[0];
+
+ // If parent changed, restructure
+ if ($newParent->ID !== $parentId) {
+ wp_update_post([
+ 'ID' => $newParent->ID,
+ 'post_parent' => 0,
+ 'menu_order' => 0
+ ]);
+
+ wp_update_post([
+ 'ID' => $parentId,
+ 'post_parent' => $newParent->ID
+ ]);
+
+ foreach ($allPosts as $index => $post) {
+ if ($index === 0) continue;
+
+ wp_update_post([
+ 'ID' => $post->ID,
+ 'post_parent' => $newParent->ID,
+ 'menu_order' => $index
+ ]);
+
+ $this->getOrCreateTerm($post->ID, (string)$index, 'number');
+ }
+ } else {
+ // Just update menu_order
+ foreach ($allPosts as $index => $post) {
+ if ($index === 0) continue;
+
+ wp_update_post([
+ 'ID' => $post->ID,
+ 'menu_order' => $index
+ ]);
+
+ $this->getOrCreateTerm($post->ID, (string)$index, 'number');
+ }
+ }
+
+ // Calculate and set timeline taxonomy (time since previous post)
+ $previousPost = null;
+ foreach ($allPosts as $index => $post) {
+ if ($index === 0) {
+ // Parent post - no timeline term (it's the baseline)
+ wp_set_object_terms($post->ID, [], BASE . 'timeline', false);
+ $previousPost = $post;
+ continue;
+ }
+
+ $timelineTerm = $this->calculateTimelineTerm($previousPost, $post);
+ if ($timelineTerm) {
+ $this->getorCreateTerm($post->ID, $timelineTerm, 'timeline');
+ }
+
+ $previousPost = $post;
+ }
+
+ $this->updateTimelineLatestDate($newParent->ID);
+ }
+
+ private function updateTimelineLatestDate(int $parentId): void
+ {
+ $parent = get_post($parentId);
+ if (!$parent) return;
+
+ // Get all posts in timeline
+ $children = get_posts([
+ 'post_type' => get_post_type($parentId),
+ 'post_parent' => $parentId,
+ 'post_status' => ['publish', 'draft'],
+ 'posts_per_page' => -1,
+ 'orderby' => 'date',
+ 'order' => 'DESC', // Get newest first
+ 'fields' => 'ids'
+ ]);
+
+ $allPostIds = array_merge([$parentId], $children);
+
+ // Get all timestamps
+ $timestamps = array_map(function($id) {
+ $post = get_post($id);
+ return $post ? strtotime($post->post_date) : 0;
+ }, $allPostIds);
+
+ $latestTimestamp = max($timestamps);
+
+ // Store as UNIX timestamp
+ update_post_meta($parentId, BASE . 'latest_date', $latestTimestamp);
+ }
+
+ private function calculateTimelineTerm(\WP_Post $previousPost, \WP_Post $currentPost): ?string
+ {
+ $previousDate = strtotime($previousPost->post_date);
+ $currentDate = strtotime($currentPost->post_date);
+
+ if (!$previousDate || !$currentDate || $currentDate <= $previousDate) {
+ return null;
+ }
+
+ // Calculate difference in days
+ $daysDiff = floor(($currentDate - $previousDate) / (60 * 60 * 24));
+
+ // Convert to weeks
+ $weeks = floor($daysDiff / 7);
+
+ // If less than 16 weeks, use weeks
+ if ($weeks < 16) {
+ if ($weeks === 0) {
+ return null; // Same week, no term
+ }
+ return $weeks === 1 ? '1 Week' : $weeks . ' Weeks';
+ }
+
+ // 16+ weeks, calculate months
+ // Using actual month calculation rather than weeks/4
+ $previousDateTime = new \DateTime($previousPost->post_date);
+ $currentDateTime = new \DateTime($currentPost->post_date);
+ $interval = $previousDateTime->diff($currentDateTime);
+
+ $months = ($interval->y * 12) + $interval->m;
+
+ if ($months === 0) {
+ // Edge case: technically less than a full month but 16+ weeks
+ return $weeks . ' Weeks';
+ }
+
+ return $months === 1 ? '1 Month' : $months . ' Months';
+ }
+
+ private function getOrCreateTerm(int $postID, string $termName, string $taxonomy): void
+ {
+ $taxonomy = jvbCheckBase($taxonomy);
+ $term = get_term_by('name', $termName, $taxonomy);
+
+ if (!$term) {
+ $result = wp_insert_term($termName, $taxonomy);
+ if (is_wp_error($result)) {
+ return;
+ }
+ $termID = $result['term_id'];
+ } else {
+ $termID = $term->term_id;
+ }
+
+ if ($termID) {
+ wp_set_object_terms($postID, [$termID], $taxonomy, false);
+ }
+ }
+
private function savePostFields(int $postId, array $postData): bool
{
$content = $postData['content'] ?? '';
@@ -325,9 +553,11 @@
$results = [];
foreach ($files as $img) {
+ $title = $this->generatePostTitle($data['content']);
$postId = wp_insert_post([
'post_type' => $this->postType,
- 'post_title' => $this->generatePostTitle($data['content']),
+ 'post_title' => $title,
+ 'post_slug' => sanitize_title($title),
'post_status' => 'draft',
'post_author' => $operation->userId,
]);
@@ -342,202 +572,6 @@
}
// ─────────────────────────────────────────────────────────────
- // Timeline Processing
- // ─────────────────────────────────────────────────────────────
-
- private function processTimelinePost(int $parentId, array $postData): array
- {
- if (!$this->verifyOwnership($parentId)) {
- return ['success' => false, 'message' => 'No permission'];
- }
-
- $content = $postData['content'];
- $this->initTimelineFields($content);
-
- $parentPost = get_post($parentId);
- $parentIsPublished = ($parentPost->post_status === 'publish');
-
- // Extract shared data (excluding post_thumbnail)
- $sharedData = array_filter($postData, function ($key) {
- return in_array($key, $this->timelineSharedFields)
- && !in_array($key, ['content', 'user'])
- && $key !== 'post_thumbnail';
- }, ARRAY_FILTER_USE_KEY);
-
- if (!isset($sharedData['post_title']) && isset($postData['timeline'][0]['post_title'])) {
- $sharedData['post_title'] = $postData['timeline'][0]['post_title'];
- }
-
- if (!isset($postData['timeline']) || !is_array($postData['timeline'])) {
- return ['success' => false, 'message' => 'No timeline data'];
- }
-
- // Validate parent is in timeline
- $index = array_search((string)$parentId, array_column($postData['timeline'], 'id'));
- if ($index === false) {
- return ['success' => false, 'message' => 'Missing parent id'];
- }
-
- // Handle parent reordering if needed
- if ($index !== 0) {
- $parentId = $this->reorderTimelineParent($parentId, $postData['timeline'], $index);
- $parentPost = get_post($parentId);
- $parentIsPublished = ($parentPost->post_status === 'publish');
- }
-
- // Shared taxonomies (excluding title and thumbnail)
- $sharedTaxonomies = array_filter($sharedData, function ($key) {
- return !in_array($key, ['post_title', 'post_thumbnail']);
- }, ARRAY_FILTER_USE_KEY);
-
- $existingChildren = get_children([
- 'post_parent' => $parentId,
- 'orderby' => 'menu_order',
- 'post_status' => ['publish', 'draft'],
- 'fields' => 'ids',
- ]);
-
- $errors = [];
- $success = [];
-
- foreach ($postData['timeline'] as $order => $timeline) {
- $result = $this->processTimelineEntry(
- $timeline,
- $order,
- $parentId,
- $parentIsPublished,
- $sharedTaxonomies,
- $existingChildren,
- $content
- );
-
- if ($result['success']) {
- $success[] = $result;
- if (isset($result['child_id']) && in_array($result['child_id'], $existingChildren)) {
- unset($existingChildren[array_search($result['child_id'], $existingChildren)]);
- }
- } else {
- $errors[] = $result;
- }
- }
-
- // Trash orphaned children
- foreach ($existingChildren as $orphanId) {
- wp_trash_post($orphanId);
- }
-
- return [
- 'success' => empty($errors),
- 'updated' => count($success),
- 'errors' => $errors,
- ];
- }
-
- private function processTimelineEntry(
- array $timeline,
- int $order,
- int $parentId,
- bool $parentIsPublished,
- array $sharedTaxonomies,
- array &$existingChildren,
- string $content
- ): array {
- $isParent = ((int)($timeline['id'] ?? 0) === $parentId);
-
- // Get unique fields for this entry
- $allowedFields = array_filter($timeline, function ($key) {
- return in_array($key, $this->timelineUniqueFields) && !in_array($key, ['content', 'user']);
- }, ARRAY_FILTER_USE_KEY);
-
- // Determine title
- $providedTitle = $timeline['post_title'] ?? '';
- $autoPattern = '/^.+Treatment #?\d+$/';
-
- if ($isParent) {
- $allowedFields['post_title'] = $providedTitle ?: ($sharedTaxonomies['post_title'] ?? get_post($parentId)->post_title);
- } else {
- if (empty($providedTitle) || preg_match($autoPattern, $providedTitle)) {
- $allowedFields['post_title'] = 'Treatment ' . $order;
- } else {
- $allowedFields['post_title'] = $providedTitle;
- }
- }
-
- $allowedFields = array_merge($sharedTaxonomies, $allowedFields);
-
- // Create child if needed
- $childId = $timeline['id'] ?? null;
- if (!$childId || !is_numeric($childId)) {
- $childId = wp_insert_post([
- 'post_author' => $this->userId,
- 'post_type' => jvbCheckBase($content),
- 'post_title' => $allowedFields['post_title'],
- 'post_parent' => $parentId,
- 'menu_order' => $order,
- 'post_status' => $parentIsPublished ? 'publish' : 'draft',
- ]);
-
- if (!$childId || is_wp_error($childId)) {
- return ['success' => false, 'message' => 'Could not create child post'];
- }
- }
-
- // Update post
- $postUpdates = ['ID' => $childId];
- if (!$isParent) {
- $postUpdates['menu_order'] = $order;
- if ($parentIsPublished) {
- $currentPost = get_post($childId);
- if ($currentPost && $currentPost->post_status !== 'publish') {
- $postUpdates['post_status'] = 'publish';
- }
- }
- }
-
- if (isset($allowedFields['post_title'])) {
- $postUpdates['post_title'] = $allowedFields['post_title'];
- unset($allowedFields['post_title']);
- }
-
- wp_update_post($postUpdates);
-
- // Save meta fields
- if (!empty($allowedFields)) {
- $meta = new MetaManager($childId, 'post');
- $meta->setAll($allowedFields);
- }
-
- return ['success' => true, 'child_id' => $childId];
- }
-
- private function reorderTimelineParent(int $currentParentId, array $timeline, int $currentIndex): int
- {
- $newParentId = $timeline[0]['id'] ?? null;
-
- if (!is_numeric($newParentId) || (int)$newParentId <= 0) {
- return $currentParentId;
- }
-
- $newParentId = (int)$newParentId;
-
- // Make new parent a top-level post
- wp_update_post(['ID' => $newParentId, 'post_parent' => 0]);
-
- // Make old parent a child
- wp_update_post(['ID' => $currentParentId, 'post_parent' => $newParentId]);
-
- // Move existing children to new parent
- $existingChildren = get_children(['post_parent' => $currentParentId, 'fields' => 'ids']);
- foreach ($existingChildren as $childId) {
- if ($childId !== $newParentId) {
- wp_update_post(['ID' => $childId, 'post_parent' => $newParentId]);
- }
- }
-
- return $newParentId;
- }
-
- // ─────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────
--
Gitblit v1.10.0