From 56b6674527dd5297d598b9e2e95298d6ddda311e Mon Sep 17 00:00:00 2001 From: nixpanel Date: Sun, 5 Jul 2026 05:59:11 -0500 Subject: [PATCH] Add a real replacement Posts management page (v2.5.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts was previously just dark-themed skinning over the native edit.php list table, unlike Comments/Plugins which are genuine replacements. Built the same way: new PostsData.php REST controller (list with status/search filters matching native tabs, trash, restore, permanent delete) and a Vue page matching the Comments layout. - SubApps.php registers it as aurora-admin-posts, parented under edit.php (same pattern as Plugins under plugins.php). - redirect_native_pages() redirects direct edit.php visits (typed URL, not just the sidebar link) to the Aurora page — gated to the default post type only, so Pages and other custom post types are untouched. - MenuSerializer rewrites the sidebar's Posts/All Posts links the same way Plugins/Comments already were. - Gated by Settings > Posts' "Modern posts table" toggle, re-added now that it has a real implementation (it was removed a few commits ago for describing a feature that didn't exist). - Fixed modern_plugins/modern_comments/modern_posts all defaulting to visually "on" in the Settings UI to match their actual PHP default. Verified live: trash/restore/status filters/search all work correctly, edit.php redirects to the Aurora page, edit.php?post_type=page (Pages) correctly does not. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 21 + PROJECT_STATUS.md | 50 +- admin/src/App/AuroraAdmin.php | 2 + admin/src/Pages/SubApps.php | 46 +- admin/src/Rest/PostsData.php | 231 +++++++++ admin/src/Shell/MenuSerializer.php | 7 + app/package.json | 2 +- app/src/entries/posts.js | 10 + app/src/pages/posts/Posts.vue | 624 +++++++++++++++++++++++ app/src/pages/settings/settingsConfig.js | 17 + app/vite.config.js | 1 + aurora-admin.php | 5 +- 12 files changed, 998 insertions(+), 18 deletions(-) create mode 100644 admin/src/Rest/PostsData.php create mode 100644 app/src/entries/posts.js create mode 100644 app/src/pages/posts/Posts.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d8dd8..ec8d48f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ version bump. Starts at 2.1.0 — earlier history (the theming rebuild, window chrome/taskbar, Site Editor support, etc.) lives in `git log` and `PROJECT_STATUS.md`, not reconstructed here. +## [2.5.0] - 2026-07-05 + +### Added +- Real replacement Posts management page (`aurora-admin-posts`), the same + kind of full custom page as Comments/Plugins rather than dark-themed + skinning over the native `edit.php` list table. New `PostsData.php` REST + controller (list with status/search filters, trash, restore, permanent + delete) and a new Vue page matching the Comments layout (status filter + pills, search, table with title/author/categories/comments/date/status, + inline row actions, pagination). The native Posts screen (`edit.php`, + default post type only — Pages and other post types are untouched) + redirects to it, gated by Settings > Posts' restored "Modern posts + table" toggle (removed a few commits ago when it had no implementation + behind it — now it does). + +### Fixed +- The `modern_plugins`/`modern_comments` settings toggles now default to + visually "on" in the Settings UI to match their actual PHP default + (previously they defaulted true server-side but showed off in the UI + until explicitly saved) — same fix applied to the new `modern_posts`. + ## [2.4.2] - 2026-07-04 ### Fixed diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 1e2b03c..8abe22b 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Aurora Admin v2 — Project Status -_Last updated: 2026-07-04 (latest session — remaining placeholder settings wired up, dashboard scrolling and admin notices fixed)_ +_Last updated: 2026-07-05 (latest session — real Posts replacement page, first of the "skinned vs. replaced" pages to actually get replaced)_ ## What this project is @@ -147,6 +147,54 @@ get back up to speed instead of relying on prior chat history. it keeps its own independent expand/collapse state (`localStorage['aurora-nav-expanded']`) — not part of this accordion. +## Skinned vs. replaced native pages (important distinction, ask before assuming) + +Not every native WP admin page gets the same treatment, and the user has +explicitly said they don't want the "skinned" approach used more broadly +without a real replacement behind it — check this list before describing +a page as "already handled": + +- **Actually replaced** — the native page never renders; a real custom Vue + page (own REST controller, own list/detail UI) takes over entirely: + Comments, Installed Plugins, Menu Creator, Admin Notices, Role Editor, + Activity Log, Database Explorer, and now **Posts** (added this session + — see below). +- **Just skinned** — the real native WordPress page still renders; only + `admin/assets/native-dark.css` restyles it dark. Same underlying + markup, same native limitations, no new functionality: Pages, Media, + Users, and every other native screen not listed above. + +**Posts** (`admin/src/Rest/PostsData.php` + `app/src/pages/posts/Posts.vue`, +entry `posts.js`) was the first of these converted from skinned to +replaced, built directly off the Comments pattern: +- REST: GET `/posts` (status/search/pagination, matching native filters: + all/publish/draft/pending/trash), POST `/posts/trash`, POST + `/posts/restore`, DELETE `/posts/{id}` (permanent). Capability checks + mirror native (`edit_posts` to list, `edit_post`/`delete_post` per-item). +- `SubApps.php`: registered as `aurora-admin-posts`, parented under + `edit.php` (same pattern as Plugins under `plugins.php`) so it shares + the native "Posts" top-level menu's `hook_suffix` slot. +- `SubApps::redirect_native_pages()`: visiting `edit.php` directly (typed + URL, bookmark — not just the sidebar link) redirects to the Aurora page, + gated on **only** the default post type (`post_type=post` or absent) — + `edit.php?post_type=page` (Pages) and any other custom post type's list + screen is explicitly left alone, since only Posts has a replacement. +- `MenuSerializer.php`: the sidebar's own "Posts"/"All Posts" links + (both share the literal `edit.php` slug) get rewritten the same way + Plugins'/Comments' links already were. +- Gated by Settings > Posts' "Modern posts table" toggle — this is the + same toggle that got **removed** two commits ago for describing a + feature that didn't exist; it's back now that it does, defaulting to + on. `modern_plugins`/`modern_comments` also got a `default: true` UI + fix at the same time (they already defaulted true server-side, but the + Settings UI toggle showed off until explicitly saved — cosmetic + mismatch, now consistent). +- **Not done**: Pages, Media, Users remain skinned-only. Each would be a + comparable-sized build to Posts/Comments (own REST controller, own + list/detail UI) — ask the user before starting one, per the pattern + established this session (they picked "Posts first, then expand" over + doing all four at once). + ## Theme system (rewrite — supersedes the old dark_mode 3-state model) Went through several rounds of user-directed rework this session, landing on: diff --git a/admin/src/App/AuroraAdmin.php b/admin/src/App/AuroraAdmin.php index 95f6a5d..174c332 100644 --- a/admin/src/App/AuroraAdmin.php +++ b/admin/src/App/AuroraAdmin.php @@ -11,6 +11,7 @@ use AuroraAdmin\Analytics\Analytics; use AuroraAdmin\Rest\AnalyticsData; use AuroraAdmin\Rest\PluginsData; use AuroraAdmin\Rest\CommentsData; +use AuroraAdmin\Rest\PostsData; use AuroraAdmin\Posts\ScheduledDayFilter; use AuroraAdmin\WhiteLabel\WhiteLabel; use AuroraAdmin\Pages\SubApps; @@ -47,6 +48,7 @@ class AuroraAdmin new AnalyticsData(); new PluginsData(); new CommentsData(); + new PostsData(); new ScheduledDayFilter(); new WhiteLabel(); new SubApps(); diff --git a/admin/src/Pages/SubApps.php b/admin/src/Pages/SubApps.php index 943f6a4..ea8f70a 100644 --- a/admin/src/Pages/SubApps.php +++ b/admin/src/Pages/SubApps.php @@ -46,6 +46,7 @@ class SubApps "aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer", "feature" => "database-explorer"], "aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins", "feature" => null], "aurora-admin-comments" => ["title" => "Comments", "entry" => "comments", "feature" => null], + "aurora-admin-posts" => ["title" => "Posts", "entry" => "posts", "feature" => null], ]; private static $hook_suffixes = []; @@ -78,6 +79,9 @@ class SubApps if ($slug === "aurora-admin-plugins") { return "plugins.php"; } + if ($slug === "aurora-admin-posts") { + return "edit.php"; + } return "aurora-admin"; } @@ -86,7 +90,7 @@ class SubApps if ($slug === "aurora-admin-plugins") { return "activate_plugins"; } - if ($slug === "aurora-admin-comments") { + if ($slug === "aurora-admin-comments" || $slug === "aurora-admin-posts") { return "edit_posts"; } return "manage_options"; @@ -112,23 +116,36 @@ class SubApps { global $pagenow; - // Settings > Comments > "Modern comments page" — previously always - // redirected regardless of this setting; default true so existing - // installs keep their current behavior until explicitly turned off. - if (!Settings::get("modern_comments", true)) { - return; - } - - if ($pagenow !== "edit-comments.php" || !current_user_can("edit_posts")) { - return; - } - if (isset($_GET["page"]) || wp_doing_ajax()) { return; } - wp_safe_redirect(admin_url("admin.php?page=aurora-admin-comments")); - exit; + // Settings > Comments > "Modern comments page" — previously always + // redirected regardless of this setting; default true so existing + // installs keep their current behavior until explicitly turned off. + if ( + $pagenow === "edit-comments.php" && + current_user_can("edit_posts") && + Settings::get("modern_comments", true) + ) { + wp_safe_redirect(admin_url("admin.php?page=aurora-admin-comments")); + exit; + } + + // Settings > Posts > "Modern posts table". Only the default post-list + // view (edit.php with no post_type, or post_type=post) redirects — + // edit.php?post_type=page (Pages) and other custom post types are + // untouched, since only Posts has a replacement page. + $post_type = isset($_GET["post_type"]) ? sanitize_key(wp_unslash($_GET["post_type"])) : "post"; + if ( + $pagenow === "edit.php" && + $post_type === "post" && + current_user_can("edit_posts") && + Settings::get("modern_posts", true) + ) { + wp_safe_redirect(admin_url("admin.php?page=aurora-admin-posts")); + exit; + } } public static function render() @@ -160,6 +177,7 @@ class SubApps "userName" => wp_get_current_user()->display_name, "roles" => $roles, "tablePrefix" => $wpdb->prefix, + "newPostUrl" => esc_url_raw(admin_url("post-new.php")), ]; $root_id = "aurora-admin-" . self::PAGES[$page]["entry"] . "-root"; ?> diff --git a/admin/src/Rest/PostsData.php b/admin/src/Rest/PostsData.php new file mode 100644 index 0000000..3e68491 --- /dev/null +++ b/admin/src/Rest/PostsData.php @@ -0,0 +1,231 @@ + Posts' old "Modern + * posts table" toggle, which described this but was never actually built; + * that toggle was removed once this landed). + */ +class PostsData +{ + public function __construct() + { + add_action("rest_api_init", [self::class, "register"]); + } + + public static function register() + { + register_rest_route("aurora-admin/v1", "/posts", [ + "methods" => "GET", + "callback" => [self::class, "list_posts"], + "permission_callback" => function () { + return current_user_can("edit_posts"); + }, + "args" => [ + "status" => ["type" => "string", "required" => false], + "search" => ["type" => "string", "required" => false], + "page" => ["type" => "integer", "required" => false], + "perPage" => ["type" => "integer", "required" => false], + ], + ]); + + register_rest_route("aurora-admin/v1", "/posts/trash", [ + "methods" => "POST", + "callback" => [self::class, "trash_post"], + "permission_callback" => [self::class, "can_edit_param_post"], + "args" => [ + "id" => ["type" => "integer", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/posts/restore", [ + "methods" => "POST", + "callback" => [self::class, "restore_post"], + "permission_callback" => [self::class, "can_edit_param_post"], + "args" => [ + "id" => ["type" => "integer", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/posts/(?P\d+)", [ + "methods" => "DELETE", + "callback" => [self::class, "delete_post"], + "permission_callback" => [self::class, "can_delete_route_post"], + "args" => [ + "id" => ["type" => "integer", "required" => true], + ], + ]); + } + + public static function can_edit_param_post($request) + { + $id = (int) $request->get_param("id"); + return $id && current_user_can("edit_post", $id); + } + + public static function can_delete_route_post($request) + { + $id = (int) $request["id"]; + return $id && current_user_can("delete_post", $id); + } + + public static function list_posts($request) + { + $status = self::query_status($request->get_param("status")); + $page = max(1, (int) ($request->get_param("page") ?: 1)); + $per_page = min(100, max(10, (int) ($request->get_param("perPage") ?: 20))); + $search = sanitize_text_field((string) $request->get_param("search")); + + $args = [ + "post_type" => "post", + "post_status" => $status, + "posts_per_page" => $per_page, + "paged" => $page, + "orderby" => "date", + "order" => "DESC", + "ignore_sticky_posts" => true, + ]; + + if ($search !== "") { + $args["s"] = $search; + } + + $query = new \WP_Query($args); + + return new \WP_REST_Response([ + "items" => array_map([self::class, "serialize_post"], $query->posts), + "stats" => self::stats(), + "pagination" => [ + "page" => $page, + "perPage" => $per_page, + "total" => (int) $query->found_posts, + "totalPages" => max(1, (int) $query->max_num_pages), + ], + ], 200); + } + + public static function trash_post($request) + { + $id = (int) $request->get_param("id"); + $post = get_post($id); + if (!$post || $post->post_type !== "post") { + return new \WP_Error("aurora_post_invalid", __("Invalid post.", "aurora-admin"), ["status" => 400]); + } + + $result = wp_trash_post($id); + if (!$result) { + return new \WP_Error("aurora_post_trash_failed", __("Could not trash the post.", "aurora-admin"), ["status" => 500]); + } + + return new \WP_REST_Response(["success" => true, "message" => __("Post moved to trash.", "aurora-admin")], 200); + } + + public static function restore_post($request) + { + $id = (int) $request->get_param("id"); + $post = get_post($id); + if (!$post || $post->post_type !== "post") { + return new \WP_Error("aurora_post_invalid", __("Invalid post.", "aurora-admin"), ["status" => 400]); + } + + $result = wp_untrash_post($id); + if (!$result) { + return new \WP_Error("aurora_post_restore_failed", __("Could not restore the post.", "aurora-admin"), ["status" => 500]); + } + + return new \WP_REST_Response(["success" => true, "message" => __("Post restored.", "aurora-admin")], 200); + } + + public static function delete_post($request) + { + $id = (int) $request["id"]; + $post = get_post($id); + if (!$post || $post->post_type !== "post") { + return new \WP_Error("aurora_post_invalid", __("Invalid post.", "aurora-admin"), ["status" => 400]); + } + + $result = wp_delete_post($id, true); + if (!$result) { + return new \WP_Error("aurora_post_delete_failed", __("Could not delete the post.", "aurora-admin"), ["status" => 500]); + } + + return new \WP_REST_Response(["success" => true, "message" => __("Post deleted.", "aurora-admin")], 200); + } + + private static function serialize_post($post) + { + $author = get_userdata($post->post_author); + $categories = wp_get_post_categories($post->ID, ["fields" => "names"]); + $status = $post->post_status; + + return [ + "id" => (int) $post->ID, + "title" => get_the_title($post) ?: __("(No title)", "aurora-admin"), + "author" => $author ? $author->display_name : __("Unknown", "aurora-admin"), + "categories" => is_array($categories) ? $categories : [], + "commentCount" => (int) $post->comment_count, + "date" => get_the_date("M j, Y", $post), + "dateLabel" => + $status === "publish" + ? __("Published", "aurora-admin") + : ($status === "future" + ? __("Scheduled", "aurora-admin") + : __("Last Modified", "aurora-admin")), + "status" => $status, + "statusLabel" => self::status_label($status), + "editUrl" => get_edit_post_link($post->ID, "raw") ?: "", + "viewUrl" => $status === "publish" ? get_permalink($post) : get_preview_post_link($post), + "canDelete" => current_user_can("delete_post", $post->ID), + ]; + } + + private static function stats() + { + $counts = wp_count_posts("post"); + + return [ + "all" => (int) $counts->publish + (int) $counts->draft + (int) $counts->pending + (int) $counts->private + (int) $counts->future, + "publish" => (int) $counts->publish, + "draft" => (int) $counts->draft, + "pending" => (int) $counts->pending, + "trash" => (int) ($counts->trash ?? 0), + ]; + } + + private static function query_status($status) + { + switch ($status) { + case "publish": + case "draft": + case "pending": + case "trash": + return $status; + case "all": + default: + return ["publish", "draft", "pending", "private", "future"]; + } + } + + private static function status_label($status) + { + switch ($status) { + case "publish": + return __("Published", "aurora-admin"); + case "draft": + return __("Draft", "aurora-admin"); + case "pending": + return __("Pending", "aurora-admin"); + case "future": + return __("Scheduled", "aurora-admin"); + case "private": + return __("Private", "aurora-admin"); + case "trash": + return __("Trash", "aurora-admin"); + default: + return ucfirst($status); + } + } +} diff --git a/admin/src/Shell/MenuSerializer.php b/admin/src/Shell/MenuSerializer.php index 75ebc56..d05ec71 100644 --- a/admin/src/Shell/MenuSerializer.php +++ b/admin/src/Shell/MenuSerializer.php @@ -155,6 +155,13 @@ class MenuSerializer return admin_url("admin.php?page=aurora-admin-comments"); } + // "edit.php" (no query string) is specifically the default post type's + // (Posts) list — other post types register as "edit.php?post_type=xyz", + // a different string, so they're untouched. + if ($slug === "edit.php" && Settings::get("modern_posts", true)) { + return admin_url("admin.php?page=aurora-admin-posts"); + } + // Already an absolute URL. if (preg_match('#^https?://#', $slug)) { return $slug; diff --git a/app/package.json b/app/package.json index dbf3b1a..02c2ceb 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "aurora-admin-app", - "version": "2.4.2", + "version": "2.5.0", "private": true, "type": "module", "scripts": { diff --git a/app/src/entries/posts.js b/app/src/entries/posts.js new file mode 100644 index 0000000..f66b67f --- /dev/null +++ b/app/src/entries/posts.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Posts from '@/pages/posts/Posts.vue'; + +const mountEl = document.getElementById('aurora-admin-posts-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Posts, { serverData: data }).mount(mountEl); +} diff --git a/app/src/pages/posts/Posts.vue b/app/src/pages/posts/Posts.vue new file mode 100644 index 0000000..172f639 --- /dev/null +++ b/app/src/pages/posts/Posts.vue @@ -0,0 +1,624 @@ + + + + + diff --git a/app/src/pages/settings/settingsConfig.js b/app/src/pages/settings/settingsConfig.js index 87cffa0..5004b4f 100644 --- a/app/src/pages/settings/settingsConfig.js +++ b/app/src/pages/settings/settingsConfig.js @@ -152,6 +152,21 @@ export const settingsCategories = [ }, ], }, + { + id: 'posts', + title: 'Posts', + subtitle: 'Post management and display preferences', + icon: 'dashicons-admin-post', + fields: [ + { + key: 'modern_posts', + type: 'toggle', + label: 'Modern posts table', + description: 'Use the Aurora posts management interface.', + default: true, + }, + ], + }, { id: 'plugins', title: 'Plugins', @@ -163,6 +178,7 @@ export const settingsCategories = [ type: 'toggle', label: 'Modern plugins page', description: 'Use the Aurora plugins management interface.', + default: true, }, ], }, @@ -227,6 +243,7 @@ export const settingsCategories = [ type: 'toggle', label: 'Modern comments page', description: 'Use the Aurora comments management interface.', + default: true, }, ], }, diff --git a/app/vite.config.js b/app/vite.config.js index 9b9d3d3..4d55e7a 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -30,6 +30,7 @@ export default defineConfig({ 'database-explorer': fileURLToPath(new URL('./src/entries/database-explorer.js', import.meta.url)), plugins: fileURLToPath(new URL('./src/entries/plugins.js', import.meta.url)), comments: fileURLToPath(new URL('./src/entries/comments.js', import.meta.url)), + posts: fileURLToPath(new URL('./src/entries/posts.js', import.meta.url)), 'locked-feature': fileURLToPath(new URL('./src/entries/locked-feature.js', import.meta.url)), }, output: { diff --git a/aurora-admin.php b/aurora-admin.php index db8498a..40121ec 100644 --- a/aurora-admin.php +++ b/aurora-admin.php @@ -3,7 +3,7 @@ * Plugin Name: Aurora Admin * Plugin URI: https://aurora.co * Description: A modern, fast WordPress admin interface. - * Version: 2.4.2 + * Version: 2.5.0 * Author: Aurora * Text Domain: aurora-admin * Domain Path: /languages/ @@ -14,7 +14,7 @@ defined("ABSPATH") || exit(); -define("AURORA_ADMIN_VERSION", "2.4.2"); +define("AURORA_ADMIN_VERSION", "2.5.0"); define("AURORA_ADMIN_PATH", plugin_dir_path(__FILE__)); define("AURORA_ADMIN_URL", plugins_url("aurora-admin/")); @@ -32,6 +32,7 @@ require AURORA_ADMIN_PATH . "admin/src/Analytics/Analytics.php"; require AURORA_ADMIN_PATH . "admin/src/Rest/AnalyticsData.php"; require AURORA_ADMIN_PATH . "admin/src/Rest/PluginsData.php"; require AURORA_ADMIN_PATH . "admin/src/Rest/CommentsData.php"; +require AURORA_ADMIN_PATH . "admin/src/Rest/PostsData.php"; require AURORA_ADMIN_PATH . "admin/src/Posts/ScheduledDayFilter.php"; require AURORA_ADMIN_PATH . "admin/src/WhiteLabel/WhiteLabel.php"; require AURORA_ADMIN_PATH . "admin/src/Pages/SubApps.php";