feat: change log tab with filters, revision links and clear button
ChangeLog gains query()/abilities()/clear() (filters: ability, user, result, date range - all optional and additive). LogTab/LogListTable render the "Protokoll" admin tab: WP_List_Table with pagination, links to the post/attachment/nav_menu edit screen by object_type, a revision link shown only when the user may edit the revision's parent post, and a nonce+manage_options-gated "Protokoll leeren" with a JS confirmation and highlighted 'partial' rows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Admin;
|
||||
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps -- method names are defined by WP_List_Table.
|
||||
|
||||
/**
|
||||
* List of change log entries (spec §8.3, ruling C5/Q13). WP_List_Table must be loaded
|
||||
* before this file (LogTab::render() takes care of that).
|
||||
*/
|
||||
final class LogListTable extends \WP_List_Table
|
||||
{
|
||||
private const PER_PAGE = 50;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* ability: string, user_id: int, result?: string, date_from?: string, date_to?: string
|
||||
* } $filters
|
||||
*/
|
||||
public function __construct(private ChangeLog $log, private array $filters)
|
||||
{
|
||||
parent::__construct(['singular' => 'lit_mcp_log_entry', 'plural' => 'lit_mcp_log_entries', 'ajax' => false]);
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function get_columns(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => __('Zeit (UTC)', 'wp-lit-mcp'),
|
||||
'user' => __('Benutzer', 'wp-lit-mcp'),
|
||||
'client' => __('Client', 'wp-lit-mcp'),
|
||||
'ability' => __('Tool', 'wp-lit-mcp'),
|
||||
'object' => __('Objekt', 'wp-lit-mcp'),
|
||||
'summary' => __('Zusammenfassung', 'wp-lit-mcp'),
|
||||
'revision' => __('Revision', 'wp-lit-mcp'),
|
||||
'result' => __('Ergebnis', 'wp-lit-mcp'),
|
||||
];
|
||||
}
|
||||
|
||||
public function prepare_items(): void
|
||||
{
|
||||
$page = max(1, (int) $this->get_pagenum());
|
||||
$data = $this->log->query($this->filters, self::PER_PAGE, ($page - 1) * self::PER_PAGE);
|
||||
$this->items = $data['rows'];
|
||||
$this->_column_headers = [$this->get_columns(), [], []];
|
||||
$this->set_pagination_args(['total_items' => $data['total'], 'per_page' => self::PER_PAGE]);
|
||||
}
|
||||
|
||||
public function no_items(): void
|
||||
{
|
||||
esc_html_e('Keine Einträge.', 'wp-lit-mcp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights rows with result "partial" (a write that failed after an irreversible
|
||||
* step, spec §2 principle 7/8.2) so admins notice them without reading every row.
|
||||
*
|
||||
* @param array<string, mixed> $item
|
||||
*/
|
||||
public function single_row($item): void
|
||||
{
|
||||
$style = ($item['result'] ?? '') === 'partial' ? ' style="background:#fcf3e3;"' : '';
|
||||
printf('<tr%s>', $style); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed string.
|
||||
$this->single_row_columns($item);
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $item
|
||||
* @param string $column_name
|
||||
*/
|
||||
protected function column_default($item, $column_name): string
|
||||
{
|
||||
return match ($column_name) {
|
||||
'user' => $this->userName((int) ($item['user_id'] ?? 0)),
|
||||
'object' => $this->objectLink((string) ($item['object_type'] ?? ''), (string) ($item['object_id'] ?? '')),
|
||||
'revision' => $this->revisionLink((int) ($item['revision_id'] ?? 0)),
|
||||
default => esc_html((string) ($item[$column_name] ?? '')),
|
||||
};
|
||||
}
|
||||
|
||||
private function userName(int $userId): string
|
||||
{
|
||||
$user = get_userdata($userId);
|
||||
|
||||
return esc_html(is_object($user) ? (string) $user->display_name : '#' . $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Links by object_type (ruling C5/Q13): known post types (not attachment) via
|
||||
* get_edit_post_link(); attachment via the media edit screen; nav_menu (a term id)
|
||||
* via the menus screen; settings/menu_location/anything else has no admin screen
|
||||
* to link to and is shown as plain text.
|
||||
*/
|
||||
private function objectLink(string $objectType, string $objectId): string
|
||||
{
|
||||
$label = trim($objectType . ' ' . $objectId);
|
||||
$url = $objectId !== '' && ctype_digit($objectId) ? $this->urlFor($objectType, (int) $objectId) : '';
|
||||
|
||||
return $url !== '' ? sprintf('<a href="%s">%s</a>', esc_url($url), esc_html($label)) : esc_html($label);
|
||||
}
|
||||
|
||||
private function urlFor(string $objectType, int $id): string
|
||||
{
|
||||
if ($objectType === 'attachment') {
|
||||
return get_post($id) instanceof WP_Post ? admin_url('upload.php?item=' . $id) : '';
|
||||
}
|
||||
|
||||
if ($objectType === 'nav_menu') {
|
||||
return get_term($id, 'nav_menu') instanceof WP_Term ? admin_url('nav-menus.php?menu=' . $id) : '';
|
||||
}
|
||||
|
||||
if ($objectType !== '' && post_type_exists($objectType)) {
|
||||
$link = get_edit_post_link($id, 'raw');
|
||||
|
||||
return is_string($link) ? $link : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Only shown when the user could actually act on it: revision_id set and the
|
||||
* revision's parent post is one the current user may edit (ruling C5/Q13).
|
||||
*/
|
||||
private function revisionLink(int $revisionId): string
|
||||
{
|
||||
if ($revisionId <= 0) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
$revision = get_post($revisionId);
|
||||
$parentId = $revision instanceof WP_Post ? (int) $revision->post_parent : 0;
|
||||
if ($parentId <= 0 || ! current_user_can('edit_post', $parentId)) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url(admin_url('revision.php?revision=' . $revisionId)),
|
||||
esc_html__('ansehen / wiederherstellen', 'wp-lit-mcp')
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user