feat: Protokoll tab as a core-style list table
The change summary is the wide primary column (key "title", so it cannot
be hidden) with the tool's human label below and row actions "Revision
ansehen" / "Objekt öffnen". Objects get human names ("Seite: Kontakt",
"Menü: …", "Einstellungen: Titel der Website"; deleted/trashed marked),
primed per page with one query each for posts, menus and users. Times are
relative within 24 h. Non-ok results show a badge and a marked row instead
of an Ergebnis column. Page size and columns via Screen Options; the tool
filter shows labels grouped by integration, dates have visible labels,
and an active filter offers "Filter zurücksetzen" (also in the empty state).
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:
+139
-25
@@ -5,21 +5,34 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Admin;
|
||||
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Limits;
|
||||
|
||||
/**
|
||||
* Tab "Protokoll": filterable list of change log entries, links to revisions, and
|
||||
* "Protokoll leeren" (spec §8.3).
|
||||
* "Protokoll leeren" (spec §8.3). Page size and visible columns via WordPress'
|
||||
* "Ansicht anpassen" (screen options), as on the core list screens.
|
||||
*/
|
||||
final class LogTab implements AdminTab
|
||||
{
|
||||
public const CLEAR_ACTION = 'lit_mcp_clear_log';
|
||||
|
||||
/** Screen option "Einträge pro Seite" (user option; WordPress needs the "_per_page" end). */
|
||||
public const PER_PAGE_OPTION = 'lit_mcp_log_per_page';
|
||||
public const PER_PAGE_DEFAULT = 20;
|
||||
private const RESULTS = ['ok', 'partial'];
|
||||
|
||||
private AdminPostAction $action;
|
||||
|
||||
public function __construct(private ChangeLog $log, private Limits $limits, ?callable $terminate = null)
|
||||
{
|
||||
/** Created on the screen's load hook so WordPress knows its columns for the screen options. */
|
||||
private ?LogListTable $table = null;
|
||||
|
||||
public function __construct(
|
||||
private ChangeLog $log,
|
||||
private Limits $limits,
|
||||
private IntegrationRegistry $registry,
|
||||
?callable $terminate = null
|
||||
) {
|
||||
$this->action = new AdminPostAction($terminate);
|
||||
}
|
||||
|
||||
@@ -40,6 +53,51 @@ final class LogTab implements AdminTab
|
||||
public function registerActions(): void
|
||||
{
|
||||
add_action('admin_post_' . self::CLEAR_ACTION, [$this, 'handleClear']);
|
||||
add_action('load-' . SettingsPage::SCREEN_HOOK, [$this, 'loadScreen']);
|
||||
add_filter('set_screen_option_' . self::PER_PAGE_OPTION, [$this, 'savePerPage'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load hook of the settings page: on this tab only, "Einträge pro Seite" in the screen
|
||||
* options, and the list table built early so its columns can be hidden there too
|
||||
* (WP_List_Table registers them for the screen, as on edit.php).
|
||||
*/
|
||||
public function loadScreen(): void
|
||||
{
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
|
||||
$tab = isset($_GET['tab']) ? sanitize_key(wp_unslash((string) $_GET['tab'])) : '';
|
||||
if ($tab !== $this->slug()) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_screen_option('per_page', [
|
||||
'label' => __('Einträge pro Seite', 'wp-lit-mcp'),
|
||||
'default' => self::PER_PAGE_DEFAULT,
|
||||
'option' => self::PER_PAGE_OPTION,
|
||||
]);
|
||||
$this->table = $this->createTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets WordPress save the page size (it already checked nonce and range 1–999).
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function savePerPage(mixed $status, string $option, mixed $value): mixed
|
||||
{
|
||||
return $option === self::PER_PAGE_OPTION ? (int) $value : $status;
|
||||
}
|
||||
|
||||
/** @param array<string, int|string> $filters */
|
||||
public static function hasActiveFilter(array $filters): bool
|
||||
{
|
||||
foreach ($filters as $value) {
|
||||
if ($value !== '' && $value !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return array{ability: string, user_id: int} */
|
||||
@@ -83,7 +141,7 @@ final class LogTab implements AdminTab
|
||||
return preg_match('#^\d{4}-\d{2}-\d{2}$#D', $value) === 1;
|
||||
}
|
||||
|
||||
public function render(): void
|
||||
private function createTable(): LogListTable
|
||||
{
|
||||
if (! class_exists('WP_List_Table')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
@@ -91,6 +149,23 @@ final class LogTab implements AdminTab
|
||||
|
||||
$filters = $this->filters();
|
||||
$extra = $this->extraFilters();
|
||||
$labels = [];
|
||||
foreach ($this->registry->abilities() as $ability) {
|
||||
$labels[$ability->name()] = $ability->label();
|
||||
}
|
||||
|
||||
return new LogListTable(
|
||||
$this->log,
|
||||
$filters + $extra,
|
||||
function () use ($filters, $extra, $labels): void {
|
||||
$this->renderFilterControls($filters, $extra, $labels);
|
||||
},
|
||||
$labels
|
||||
);
|
||||
}
|
||||
|
||||
public function render(): void
|
||||
{
|
||||
$days = $this->limits->logRetentionDays();
|
||||
printf(
|
||||
'<p class="description">%s %s</p>',
|
||||
@@ -113,9 +188,7 @@ final class LogTab implements AdminTab
|
||||
echo '<form method="get">';
|
||||
printf('<input type="hidden" name="page" value="%s">', esc_attr(SettingsPage::SLUG));
|
||||
printf('<input type="hidden" name="tab" value="%s">', esc_attr($this->slug()));
|
||||
$table = new LogListTable($this->log, $filters + $extra, function () use ($filters, $extra): void {
|
||||
$this->renderFilterControls($filters, $extra);
|
||||
});
|
||||
$table = $this->table ?? $this->createTable();
|
||||
$table->prepare_items();
|
||||
$table->display();
|
||||
echo '</form>';
|
||||
@@ -124,20 +197,26 @@ final class LogTab implements AdminTab
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{ability: string, user_id: int} $filters
|
||||
* @param array{ability: string, user_id: int} $filters
|
||||
* @param array{result: string, date_from: string, date_to: string} $extra
|
||||
* @param array<string, string> $labels ability name => label
|
||||
*/
|
||||
private function renderFilterControls(array $filters, array $extra): void
|
||||
private function renderFilterControls(array $filters, array $extra, array $labels): void
|
||||
{
|
||||
self::filterLabel('ability', __('Nach Tool filtern', 'wp-lit-mcp'));
|
||||
echo '<select name="ability" id="lit-mcp-filter-ability"><option value="">'
|
||||
. esc_html__('Alle Tools', 'wp-lit-mcp') . '</option>';
|
||||
foreach ($this->log->abilities() as $ability) {
|
||||
printf(
|
||||
'<option value="%1$s"%2$s>%1$s</option>',
|
||||
esc_attr($ability),
|
||||
selected($filters['ability'], $ability, false)
|
||||
);
|
||||
foreach ($this->toolOptionGroups() as $group => $abilities) {
|
||||
printf('<optgroup label="%s">', esc_attr($group));
|
||||
foreach ($abilities as $ability) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($ability),
|
||||
selected($filters['ability'], $ability, false),
|
||||
esc_html($labels[$ability] ?? $ability)
|
||||
);
|
||||
}
|
||||
echo '</optgroup>';
|
||||
}
|
||||
echo '</select>';
|
||||
|
||||
@@ -163,18 +242,53 @@ final class LogTab implements AdminTab
|
||||
}
|
||||
echo '</select>';
|
||||
|
||||
self::filterLabel('date-from', __('Von Datum', 'wp-lit-mcp'));
|
||||
printf(
|
||||
'<input type="date" name="date_from" id="lit-mcp-filter-date-from" value="%s">',
|
||||
esc_attr($extra['date_from'])
|
||||
);
|
||||
self::filterLabel('date-to', __('Bis Datum', 'wp-lit-mcp'));
|
||||
printf(
|
||||
'<input type="date" name="date_to" id="lit-mcp-filter-date-to" value="%s">',
|
||||
esc_attr($extra['date_to'])
|
||||
);
|
||||
foreach (['date_from' => __('von', 'wp-lit-mcp'), 'date_to' => __('bis', 'wp-lit-mcp')] as $name => $label) {
|
||||
$id = 'lit-mcp-filter-' . str_replace('_', '-', $name);
|
||||
printf(
|
||||
'<span class="lit-mcp-date"><label for="%1$s">%2$s</label>'
|
||||
. '<input type="date" name="%3$s" id="%1$s" value="%4$s"></span>',
|
||||
esc_attr($id),
|
||||
esc_html($label),
|
||||
esc_attr($name),
|
||||
esc_attr($extra[$name])
|
||||
);
|
||||
}
|
||||
|
||||
submit_button(__('Filtern', 'wp-lit-mcp'), '', 'filter_action', false, ['id' => 'lit-mcp-filter-submit']);
|
||||
|
||||
if (self::hasActiveFilter($filters + $extra)) {
|
||||
printf(
|
||||
'<a class="lit-mcp-reset" href="%s">%s</a>',
|
||||
esc_url(SettingsPage::url($this->slug())),
|
||||
esc_html__('Filter zurücksetzen', 'wp-lit-mcp')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The tools found in the log, grouped like the "Tools" tab (integration order);
|
||||
* tools no longer available land in "Weitere".
|
||||
*
|
||||
* @return array<string, list<string>> group label => ability names
|
||||
*/
|
||||
private function toolOptionGroups(): array
|
||||
{
|
||||
$logged = array_flip($this->log->abilities());
|
||||
$groupLabels = $this->registry->labels();
|
||||
$groups = [];
|
||||
foreach ($this->registry->grouped() as $group => $abilities) {
|
||||
foreach ($abilities as $ability) {
|
||||
if (isset($logged[$ability->name()])) {
|
||||
$groups[$groupLabels[$group] ?? $group][] = $ability->name();
|
||||
unset($logged[$ability->name()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($logged !== []) {
|
||||
$groups[__('Weitere', 'wp-lit-mcp')] = array_map('strval', array_keys($logged));
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
private static function filterLabel(string $field, string $text): void
|
||||
|
||||
Reference in New Issue
Block a user