Add a real replacement Posts management page (v2.5.0)
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 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d8bf1ae049
commit
56b6674527
@@ -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
|
||||
|
||||
+49
-1
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
+32
-14
@@ -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";
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Rest;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* REST endpoints for Aurora's custom Posts page — a real replacement for
|
||||
* the native edit.php list table (unlike Settings > 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<id>\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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aurora-admin-app",
|
||||
"version": "2.4.2",
|
||||
"version": "2.5.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const posts = ref([]);
|
||||
const stats = ref({ all: 0, publish: 0, draft: 0, pending: 0, trash: 0 });
|
||||
const pagination = ref({ page: 1, perPage: 20, total: 0, totalPages: 1 });
|
||||
const loading = ref(true);
|
||||
const busyId = ref(0);
|
||||
const error = ref('');
|
||||
const notice = ref('');
|
||||
const search = ref('');
|
||||
const status = ref('all');
|
||||
|
||||
const apiBase = computed(() => `${props.serverData.restUrl}aurora-admin/v1`);
|
||||
|
||||
const request = async (path, options = {}) => {
|
||||
const response = await fetch(`${apiBase.value}${path}`, {
|
||||
credentials: 'same-origin',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': props.serverData.restNonce,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Request failed');
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
status: status.value,
|
||||
search: search.value.trim(),
|
||||
page: String(pagination.value.page),
|
||||
perPage: String(pagination.value.perPage),
|
||||
});
|
||||
const data = await request(`/posts?${params.toString()}`);
|
||||
posts.value = data.items || [];
|
||||
stats.value = data.stats || stats.value;
|
||||
pagination.value = data.pagination || pagination.value;
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Could not load posts';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(load);
|
||||
|
||||
let searchTimer = 0;
|
||||
watch(search, () => {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
pagination.value.page = 1;
|
||||
load();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
const setStatus = (next) => {
|
||||
status.value = next;
|
||||
pagination.value.page = 1;
|
||||
load();
|
||||
};
|
||||
|
||||
const filters = computed(() => [
|
||||
{ key: 'all', label: 'All', count: stats.value.all },
|
||||
{ key: 'publish', label: 'Published', count: stats.value.publish },
|
||||
{ key: 'draft', label: 'Draft', count: stats.value.draft },
|
||||
{ key: 'pending', label: 'Pending', count: stats.value.pending },
|
||||
{ key: 'trash', label: 'Trash', count: stats.value.trash },
|
||||
]);
|
||||
|
||||
const trashPost = async (post) => {
|
||||
if (!post || busyId.value) return;
|
||||
busyId.value = post.id;
|
||||
error.value = '';
|
||||
notice.value = '';
|
||||
try {
|
||||
const data = await request('/posts/trash', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: post.id }),
|
||||
});
|
||||
notice.value = data.message || 'Post moved to trash.';
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Action failed';
|
||||
} finally {
|
||||
busyId.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const restorePost = async (post) => {
|
||||
if (!post || busyId.value) return;
|
||||
busyId.value = post.id;
|
||||
error.value = '';
|
||||
notice.value = '';
|
||||
try {
|
||||
const data = await request('/posts/restore', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: post.id }),
|
||||
});
|
||||
notice.value = data.message || 'Post restored.';
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Action failed';
|
||||
} finally {
|
||||
busyId.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const deletePost = async (post) => {
|
||||
if (!post || busyId.value) return;
|
||||
if (!window.confirm(`Permanently delete "${post.title}"? This cannot be undone.`)) return;
|
||||
busyId.value = post.id;
|
||||
error.value = '';
|
||||
notice.value = '';
|
||||
try {
|
||||
const data = await request(`/posts/${post.id}`, { method: 'DELETE' });
|
||||
notice.value = data.message || 'Post deleted.';
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Action failed';
|
||||
} finally {
|
||||
busyId.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const goPage = (page) => {
|
||||
const next = Math.min(Math.max(1, page), pagination.value.totalPages || 1);
|
||||
if (next === pagination.value.page) return;
|
||||
pagination.value.page = next;
|
||||
load();
|
||||
};
|
||||
|
||||
const rowRange = computed(() => {
|
||||
if (!pagination.value.total) return '0 to 0';
|
||||
const start = (pagination.value.page - 1) * pagination.value.perPage + 1;
|
||||
const end = Math.min(pagination.value.total, pagination.value.page * pagination.value.perPage);
|
||||
return `${start} to ${end}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="posts-page">
|
||||
<header class="posts-page__header">
|
||||
<div>
|
||||
<p class="posts-page__eyebrow">Content</p>
|
||||
<h1>Posts</h1>
|
||||
</div>
|
||||
<div class="posts-page__actions">
|
||||
<a class="button-primary" :href="serverData.newPostUrl">Add Post</a>
|
||||
<button type="button" class="icon-button" title="Refresh posts" :disabled="loading" @click="load">
|
||||
<span class="dashicons dashicons-update" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="posts-page__toolbar" aria-label="Post filters and search">
|
||||
<div class="posts-page__filters">
|
||||
<button
|
||||
v-for="filter in filters"
|
||||
:key="filter.key"
|
||||
type="button"
|
||||
class="filter-pill"
|
||||
:class="{ 'is-active': status === filter.key }"
|
||||
@click="setStatus(filter.key)"
|
||||
>
|
||||
<span>{{ filter.label }}</span>
|
||||
<strong>{{ filter.count }}</strong>
|
||||
</button>
|
||||
</div>
|
||||
<label class="posts-page__search">
|
||||
<span class="dashicons dashicons-search" />
|
||||
<input v-model="search" type="search" placeholder="Search posts..." />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div v-if="notice" class="posts-notice posts-notice--success">{{ notice }}</div>
|
||||
<div v-if="error" class="posts-notice posts-notice--error">{{ error }}</div>
|
||||
|
||||
<section class="posts-table" aria-label="Posts">
|
||||
<div class="posts-table__head">
|
||||
<div>Title</div>
|
||||
<div>Author</div>
|
||||
<div>Categories</div>
|
||||
<div>Comments</div>
|
||||
<div>Date</div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="posts-table__empty">Loading posts...</div>
|
||||
<div v-else-if="!posts.length" class="posts-table__empty">No posts match your filters.</div>
|
||||
|
||||
<article v-for="post in posts" v-else :key="post.id" class="post-row">
|
||||
<div class="post-row__title" data-label="Title">
|
||||
<a :href="post.editUrl">{{ post.title }}</a>
|
||||
<span class="status-badge" :class="`status-badge--${post.status}`">{{ post.statusLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div class="post-row__cell" data-label="Author">{{ post.author }}</div>
|
||||
|
||||
<div class="post-row__cell" data-label="Categories">
|
||||
{{ post.categories.length ? post.categories.join(', ') : '—' }}
|
||||
</div>
|
||||
|
||||
<div class="post-row__cell" data-label="Comments">{{ post.commentCount }}</div>
|
||||
|
||||
<div class="post-row__cell" data-label="Date">
|
||||
<strong>{{ post.dateLabel }}</strong>
|
||||
<span>{{ post.date }}</span>
|
||||
</div>
|
||||
|
||||
<div class="post-row__actions">
|
||||
<a class="icon-button icon-button--inline" title="Edit post" :href="post.editUrl">
|
||||
<span class="dashicons dashicons-edit" />
|
||||
</a>
|
||||
<a
|
||||
v-if="post.viewUrl"
|
||||
class="icon-button icon-button--inline"
|
||||
title="View post"
|
||||
:href="post.viewUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span class="dashicons dashicons-visibility" />
|
||||
</a>
|
||||
<button
|
||||
v-if="post.status !== 'trash'"
|
||||
type="button"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Move to trash"
|
||||
:disabled="busyId === post.id"
|
||||
@click="trashPost(post)"
|
||||
>
|
||||
<span class="dashicons dashicons-trash" />
|
||||
</button>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Restore"
|
||||
:disabled="busyId === post.id"
|
||||
@click="restorePost(post)"
|
||||
>
|
||||
<span class="dashicons dashicons-undo" />
|
||||
</button>
|
||||
<button
|
||||
v-if="post.canDelete"
|
||||
type="button"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Delete permanently"
|
||||
:disabled="busyId === post.id"
|
||||
@click="deletePost(post)"
|
||||
>
|
||||
<span class="dashicons dashicons-no-alt" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<footer v-if="!loading && posts.length" class="posts-table__footer">
|
||||
<div>Page Size: {{ pagination.perPage }}</div>
|
||||
<div>{{ rowRange }} of {{ pagination.total }}</div>
|
||||
<div class="posts-table__pager">
|
||||
<button type="button" :disabled="pagination.page <= 1" @click="goPage(1)">
|
||||
<span class="dashicons dashicons-controls-skipback" />
|
||||
</button>
|
||||
<button type="button" :disabled="pagination.page <= 1" @click="goPage(pagination.page - 1)">
|
||||
<span class="dashicons dashicons-arrow-left-alt2" />
|
||||
</button>
|
||||
<span>Page {{ pagination.page }} of {{ pagination.totalPages }}</span>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="pagination.page >= pagination.totalPages"
|
||||
@click="goPage(pagination.page + 1)"
|
||||
>
|
||||
<span class="dashicons dashicons-arrow-right-alt2" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="pagination.page >= pagination.totalPages"
|
||||
@click="goPage(pagination.totalPages)"
|
||||
>
|
||||
<span class="dashicons dashicons-controls-skipforward" />
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.posts-page {
|
||||
min-height: calc(100vh - 112px);
|
||||
padding: 0;
|
||||
color: var(--aurora-text);
|
||||
background: transparent;
|
||||
}
|
||||
.posts-page__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.posts-page__eyebrow {
|
||||
margin: 0 0 7px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.posts-page h1 {
|
||||
margin: 0;
|
||||
color: var(--aurora-text);
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.posts-page__actions,
|
||||
.posts-page__toolbar,
|
||||
.posts-page__filters,
|
||||
.post-row__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.posts-page__actions {
|
||||
gap: 10px;
|
||||
}
|
||||
.button-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-accent);
|
||||
color: var(--aurora-accent-text);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.posts-page__toolbar {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.posts-page__filters {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
padding: 0 12px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filter-pill strong {
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--aurora-frame-bg);
|
||||
color: inherit;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.filter-pill.is-active {
|
||||
border-color: var(--aurora-text-muted);
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.posts-page__search {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.posts-page__search input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
.posts-page__search input::placeholder {
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.icon-button {
|
||||
width: 42px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.icon-button:hover:not(:disabled) {
|
||||
border-color: var(--aurora-text-muted);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.icon-button--inline {
|
||||
width: 30px;
|
||||
min-height: 30px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.icon-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.posts-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.posts-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.posts-table__head,
|
||||
.post-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.6fr) minmax(120px, 0.8fr) minmax(160px, 1fr) 110px 130px 132px;
|
||||
align-items: center;
|
||||
}
|
||||
.posts-table__head {
|
||||
min-height: 48px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.79rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.posts-table__head > div {
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.post-row {
|
||||
min-height: 66px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
}
|
||||
.post-row:hover {
|
||||
background: var(--aurora-bg-subtle);
|
||||
}
|
||||
.post-row__title {
|
||||
min-width: 0;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.post-row__title a {
|
||||
overflow: hidden;
|
||||
color: var(--aurora-text);
|
||||
font-weight: 800;
|
||||
font-size: 0.86rem;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.post-row__title a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.post-row__cell {
|
||||
min-width: 0;
|
||||
padding: 14px 16px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.post-row__cell span {
|
||||
display: block;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.post-row__cell strong {
|
||||
display: block;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.status-badge {
|
||||
width: fit-content;
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
padding: 2px 9px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 850;
|
||||
box-shadow: inset 0 0 0 1px var(--aurora-border);
|
||||
}
|
||||
.post-row__actions {
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.posts-table__empty {
|
||||
padding: 38px 18px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
.posts-table__footer {
|
||||
min-height: 54px;
|
||||
border-top: 1px solid var(--aurora-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 26px;
|
||||
padding: 0 16px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.posts-table__pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.posts-table__pager button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.posts-table__pager button:not(:disabled):hover {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.posts-table__pager button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.posts-table__head,
|
||||
.post-row {
|
||||
grid-template-columns: minmax(200px, 1.6fr) minmax(100px, 0.8fr) minmax(140px, 1fr) 100px 120px 122px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.posts-table__head {
|
||||
display: none;
|
||||
}
|
||||
.post-row {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.post-row__title,
|
||||
.post-row__cell,
|
||||
.post-row__actions {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.post-row__cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
white-space: normal;
|
||||
}
|
||||
.post-row__cell::before {
|
||||
content: attr(data-label);
|
||||
color: var(--aurora-text-muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
.posts-table__footer,
|
||||
.posts-page__header,
|
||||
.posts-page__toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.posts-table__footer {
|
||||
padding: 14px 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
.posts-page__search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
+3
-2
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user