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:
nixpanel
2026-07-05 05:59:11 -05:00
co-authored by Claude Sonnet 5
parent d8bf1ae049
commit 56b6674527
12 changed files with 998 additions and 18 deletions
+21
View File
@@ -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
View File
@@ -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:
+2
View File
@@ -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
View File
@@ -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";
?>
+231
View File
@@ -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);
}
}
}
+7
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "aurora-admin-app",
"version": "2.4.2",
"version": "2.5.0",
"private": true,
"type": "module",
"scripts": {
+10
View File
@@ -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);
}
+624
View File
@@ -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>
+17
View File
@@ -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,
},
],
},
+1
View File
@@ -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
View File
@@ -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";