[Report] Add report reopen action for closed reports

- New Command_ReportReopen moderator command (extension 1206) with server
  handler that reopens resolved/dismissed reports, resets resolution state
  and notified, and records the reopening note in the audit log
- Reopen... button in the report queue action bar, gated to resolved and
  dismissed reports, with optional note prompt and tooltip
- Success confirmations for Assign, Resolve/Dismiss and Reopen now survive
  the list refresh via a pending status message instead of being clobbered
- Dispatch/auth coverage in server_moderator_role_test
This commit is contained in:
Lukas Brübach 2026-10-07 22:56:00 +02:00
parent d0991b68f6
commit a51f564b3c
9 changed files with 269 additions and 2 deletions

View file

@ -30,6 +30,7 @@
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
@ -223,12 +224,14 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
resolveButton = new QPushButton;
resolveWithNoteButton = new QPushButton;
dismissButton = new QPushButton;
reopenButton = new QPushButton;
viewReplayButton = new QPushButton;
joinGameButton = new QPushButton;
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); });
connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); });
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); });
connect(reopenButton, &QPushButton::clicked, this, &TabReport::reopenReport);
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
@ -239,6 +242,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
actionBar->addWidget(resolveButton);
actionBar->addWidget(resolveWithNoteButton);
actionBar->addWidget(dismissButton);
actionBar->addWidget(reopenButton);
actionBar->addSpacing(20);
actionBar->addWidget(viewReplayButton);
actionBar->addWidget(joinGameButton);
@ -305,6 +309,8 @@ void TabReport::retranslateUi()
resolveButton->setText(tr("Resolve"));
resolveWithNoteButton->setText(tr("Resolve with note..."));
dismissButton->setText(tr("Dismiss..."));
reopenButton->setText(tr("Reopen..."));
reopenButton->setToolTip(tr("Available for resolved or dismissed reports."));
viewReplayButton->setText(tr("View Replay"));
joinGameButton->setText(tr("Join Game"));
}
@ -336,6 +342,7 @@ void TabReport::reportListResponse(const Response &response)
refreshButton->setEnabled(true);
if (response.response_code() != Response::RespOk) {
pendingStatusMessage.clear();
statusLabel->setText(tr("Failed to load reports."));
return;
}
@ -349,6 +356,11 @@ void TabReport::reportListResponse(const Response &response)
applyFilters();
updateStats();
if (!pendingStatusMessage.isEmpty()) {
statusLabel->setText(pendingStatusMessage);
pendingStatusMessage.clear();
}
if (selectedReportIdBeforeRefresh >= 0) {
bool found = false;
for (int row = 0; row < table->rowCount(); ++row) {
@ -569,6 +581,7 @@ void TabReport::updateActionStates()
resolveButton->setEnabled(status == "open" || status == "assigned");
resolveWithNoteButton->setEnabled(status == "open" || status == "assigned");
dismissButton->setEnabled(status == "open" || status == "assigned");
reopenButton->setEnabled(status == "resolved" || status == "dismissed");
const bool hasGameId = report.game_id() > 0;
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
@ -582,6 +595,7 @@ void TabReport::setActionsEnabled(bool enabled)
resolveButton->setEnabled(enabled);
resolveWithNoteButton->setEnabled(enabled);
dismissButton->setEnabled(enabled);
reopenButton->setEnabled(enabled);
viewReplayButton->setEnabled(false);
joinGameButton->setEnabled(false);
commentButton->setEnabled(enabled);
@ -635,7 +649,7 @@ void TabReport::assignReport()
void TabReport::assignResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
statusLabel->setText(tr("Assigned."));
pendingStatusMessage = tr("Assigned.");
refreshList();
} else {
statusLabel->setText(tr("Assignment failed."));
@ -679,7 +693,46 @@ void TabReport::resolveReport(bool dismissed, bool promptNote)
void TabReport::resolveResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
statusLabel->setText(tr("Done."));
pendingStatusMessage = tr("Done.");
refreshList();
} else {
statusLabel->setText(tr("Action failed."));
updateActionStates();
}
}
void TabReport::reopenReport()
{
const int reportId = selectedReportId();
if (reportId < 0) {
return;
}
bool ok;
QString note = QInputDialog::getText(this, tr("Reopen Report"), tr("Reopening note (optional):"), QLineEdit::Normal,
QString(), &ok);
if (!ok) {
return;
}
setActionsEnabled(false);
statusLabel->setText(tr("Reopening..."));
Command_ReportReopen cmd;
cmd.set_report_id(reportId);
if (!note.isEmpty()) {
cmd.set_reopen_note(note.toStdString());
}
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabReport::reopenResponse);
client->sendCommand(pend);
}
void TabReport::reopenResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
pendingStatusMessage = tr("Reopened.");
refreshList();
} else {
statusLabel->setText(tr("Action failed."));

View file

@ -53,6 +53,8 @@ private slots:
void reportDetailsResponse(const Response &response);
void requestStats();
void statsResponse(const Response &response);
void reopenReport();
void reopenResponse(const Response &response);
private:
int selectedReportId() const;
@ -84,6 +86,7 @@ private:
QPushButton *resolveButton;
QPushButton *resolveWithNoteButton;
QPushButton *dismissButton;
QPushButton *reopenButton;
QPushButton *viewReplayButton;
QPushButton *joinGameButton;
QLabel *statusLabel;
@ -105,6 +108,7 @@ private:
QLabel *userContextNotesLabel;
QLabel *userContextRecentReportsLabel;
QString lastRequestedUser;
QString pendingStatusMessage;
QGroupBox *statsGroup;
QLabel *statsTotalLabel;
QLabel *statsTrendLabel;