sort dropdown on every column, auto-process checkbox stays visible during sorting
Build and Push Docker Image / build (push) Successful in 1m10s
Build and Push Docker Image / build (push) Successful in 1m10s
- ColumnShell: new sort row below header border with sortOptions/sortValue/onSortChange - inbox: ↓↑ scan time, ↓↑ name (dropdown moved from button row to sort row) - review: classification (default), ↓↑ scan time, ↓↑ name - queue/done: ↓↑ added, ↓↑ name (client-side sort on already-fetched arrays) - auto-process checkbox stays visible during inbox processing, progress shows below it - backend: unified GroupSort type replaces InboxSort, review bucket accepts sort param Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-25
@@ -633,19 +633,17 @@ type ReviewGroup =
|
||||
seasons: Array<{ season: number | null; episodes: ReviewItemRow[] }>;
|
||||
};
|
||||
|
||||
export type InboxSort = "scan_asc" | "scan_desc" | "name_asc" | "name_desc";
|
||||
export type GroupSort = "scan_asc" | "scan_desc" | "name_asc" | "name_desc" | "class";
|
||||
|
||||
export interface BuildReviewGroupsOpts {
|
||||
bucket: "inbox" | "review";
|
||||
sort?: InboxSort;
|
||||
sort?: GroupSort;
|
||||
}
|
||||
|
||||
function orderClause(bucket: "inbox" | "review", sort?: InboxSort): string {
|
||||
if (bucket === "review") {
|
||||
function orderClause(sort?: GroupSort): string {
|
||||
if (sort === "class")
|
||||
return `CASE rp.auto_class WHEN 'auto_heuristic' THEN 0 WHEN 'manual' THEN 1 ELSE 2 END,
|
||||
COALESCE(mi.series_name, mi.name),
|
||||
mi.season_number, mi.episode_number`;
|
||||
}
|
||||
COALESCE(mi.series_name, mi.name), mi.season_number, mi.episode_number`;
|
||||
if (sort === "scan_desc") return "mi.last_scanned_at DESC, mi.id DESC";
|
||||
if (sort === "name_asc") return "COALESCE(mi.series_name, mi.name) ASC, mi.season_number, mi.episode_number";
|
||||
if (sort === "name_desc") return "COALESCE(mi.series_name, mi.name) DESC, mi.season_number, mi.episode_number";
|
||||
@@ -657,7 +655,8 @@ export function buildReviewGroups(
|
||||
opts: BuildReviewGroupsOpts,
|
||||
): { groups: ReviewGroup[]; totalItems: number } {
|
||||
const sortedFilter = opts.bucket === "inbox" ? "rp.sorted = 0" : "rp.sorted = 1";
|
||||
const order = orderClause(opts.bucket, opts.sort);
|
||||
const defaultSort: GroupSort = opts.bucket === "inbox" ? "scan_asc" : "class";
|
||||
const order = orderClause(opts.sort ?? defaultSort);
|
||||
const rows = db
|
||||
.prepare(`
|
||||
SELECT rp.*, mi.name, mi.series_name, mi.series_key,
|
||||
@@ -728,12 +727,21 @@ export function buildReviewGroups(
|
||||
});
|
||||
}
|
||||
|
||||
// For inbox, preserve the SQL order (scan time or name). For review,
|
||||
// rank by auto_class so auto-approvable items float to the top.
|
||||
const effectiveSort = opts.sort ?? (opts.bucket === "inbox" ? "scan_asc" : "class");
|
||||
|
||||
let allGroups: ReviewGroup[];
|
||||
if (opts.bucket === "inbox") {
|
||||
// Interleave movies and series in the order their first row appeared
|
||||
// in the SQL result set so scan-time ordering stays intact.
|
||||
if (effectiveSort === "class") {
|
||||
// Class sort: rank by auto_class in JS so auto-approvable items float top.
|
||||
allGroups = [...movieGroups, ...seriesGroups].sort((a, b) => {
|
||||
const rankA = a.kind === "movie" ? autoClassRank(a.item.auto_class) : a.readyCount > 0 ? 0 : 1;
|
||||
const rankB = b.kind === "movie" ? autoClassRank(b.item.auto_class) : b.readyCount > 0 ? 0 : 1;
|
||||
if (rankA !== rankB) return rankA - rankB;
|
||||
const nameA = a.kind === "movie" ? a.item.name : a.seriesName;
|
||||
const nameB = b.kind === "movie" ? b.item.name : b.seriesName;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
} else {
|
||||
// Scan-time / name sorts: interleave movies and series in SQL row order.
|
||||
const groupOrder: ReviewGroup[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const row of rows) {
|
||||
@@ -755,15 +763,6 @@ export function buildReviewGroups(
|
||||
}
|
||||
}
|
||||
allGroups = groupOrder;
|
||||
} else {
|
||||
allGroups = [...movieGroups, ...seriesGroups].sort((a, b) => {
|
||||
const rankA = a.kind === "movie" ? autoClassRank(a.item.auto_class) : a.readyCount > 0 ? 0 : 1;
|
||||
const rankB = b.kind === "movie" ? autoClassRank(b.item.auto_class) : b.readyCount > 0 ? 0 : 1;
|
||||
if (rankA !== rankB) return rankA - rankB;
|
||||
const nameA = a.kind === "movie" ? a.item.name : a.seriesName;
|
||||
const nameB = b.kind === "movie" ? b.item.name : b.seriesName;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
}
|
||||
|
||||
const totalItems =
|
||||
@@ -784,9 +783,8 @@ app.get("/groups", (c) => {
|
||||
const bucketParam = c.req.query("bucket") ?? "review";
|
||||
const bucket = bucketParam === "inbox" ? "inbox" : "review";
|
||||
|
||||
const sortParam = c.req.query("sort") as InboxSort | undefined;
|
||||
const sort = bucket === "inbox" ? (sortParam ?? "scan_asc") : undefined;
|
||||
const { groups, totalItems } = buildReviewGroups(db, { bucket, sort });
|
||||
const sortParam = c.req.query("sort") as GroupSort | undefined;
|
||||
const { groups, totalItems } = buildReviewGroups(db, { bucket, sort: sortParam });
|
||||
const page = groups.slice(offset, offset + limit);
|
||||
|
||||
const flat: EnrichableRow[] = [];
|
||||
|
||||
Reference in New Issue
Block a user