feat(dashboard): UGC List view and References as sortable DataTables

The UGC page's List view (models and car/rocket assemblies) and an
assembly's References panel are server-side DataTables like the rest of
the dashboard: sort by clicking a column heading, in either direction,
with the page length and sort remembered per user. They still read
/api/ugc and /api/ugc/assembly/builds, so the prefix search and filters
above the list keep working; the gallery keeps its own pager and sort.

/api/ugc takes reverse=1 for a sort's other direction (the model list's
SQL order and the assembly sort both flip), and the references endpoint
sorts by id, owner, account or state. Tests cover the reversed orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 17:37:00 -05:00
parent 5bb05775ee
commit 453f728b5e
7 changed files with 191 additions and 81 deletions

View File

@@ -67,6 +67,7 @@ public:
UgcSearch search;
std::optional<IUgc::eProcessState> state;
eSort sort{ eSort::NEWEST };
bool reverse{}; // the sort's other direction (e.g. the fewest bricks first)
uint32_t offset{};
uint32_t limit{ 50 };
};

View File

@@ -50,13 +50,15 @@ namespace UgcLookupSql {
inline std::string ListOrder(const IUgcLookup::UgcListQuery& query, bool modular) {
using eSort = IUgcLookup::eSort;
const std::string id = modular ? "b.ugc_id" : "u.id";
// A sort's own direction, or the other one when the query is reversed; ties stay newest first
const auto dir = [&query](bool descending) { return descending != query.reverse ? " DESC" : " ASC"; };
switch (query.sort) {
case eSort::OLDEST: return "ORDER BY " + id + " ASC ";
case eSort::OWNER: return "ORDER BY c.name ASC, " + id + " DESC ";
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "u.filename") + " ASC, " + id + " DESC ";
case eSort::BRICKS: return modular ? "ORDER BY " + id + " DESC " : "ORDER BY u.brick_count DESC, u.id DESC ";
case eSort::TRIANGLES: return modular ? "ORDER BY " + id + " DESC " : "ORDER BY u.triangle_count DESC, u.id DESC ";
default: return "ORDER BY " + id + " DESC ";
case eSort::OLDEST: return "ORDER BY " + id + dir(false) + " ";
case eSort::OWNER: return "ORDER BY c.name" + std::string(dir(false)) + ", " + id + " DESC ";
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "u.filename") + dir(false) + ", " + id + " DESC ";
case eSort::BRICKS: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.brick_count" + std::string(dir(true)) + ", u.id DESC ";
case eSort::TRIANGLES: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.triangle_count" + std::string(dir(true)) + ", u.id DESC ";
default: return "ORDER BY " + id + dir(true) + " ";
}
}