refactor(cms): deduplicate shared admin components and fix studio batch completion
CI / check (push) Failing after 1m35s
CI / deploy (push) Skipped
CI / e2e (push) Skipped

- fix(studio): stop markBatchDone infinite recursion so batches complete
- refactor(api): merge api-response into api and drop the duplicate module
- refactor(media): extract shared media loader and URL validator
- refactor(ui): extract shared LoadingSpinner for site and admin groups
- refactor(dates): consolidate raw date formatting into formatDate util
- refactor(logs): share a single generic log-list loader across tables
- refactor(theme): merge both ColorField components and reuse contrast helpers
- refactor(import): extract shared ImportErrorBanner and SearchInput
- chore(): remove dead theme-editor-tabs after inlining tab components
This commit is contained in:
openhands committed 2026-09-05 20:58:42 +02:00
1 parent b12e405b34
commit 8c12fc6c60
93 files changed
+1447 -1005

No files matched your search

+102 -155
View File
@@ -9,6 +9,16 @@ type ListParamsInput = {
defaultPerPage?: number;
};
type SqlFragment = ReturnType<typeof sql>;
type LogListSpec<RawRow, Row> = {
select: SqlFragment;
from: SqlFragment;
searchWhere: (like: string) => SqlFragment;
orderBy: SqlFragment;
mapRow: (raw: RawRow, index: number, offset: number) => Row;
};
async function requireLogsView() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.LOGS_VIEW, session.user.rank)) {
@@ -26,11 +36,56 @@ function parsePageParams(
return { search: parsed.search.trim(), page: parsed.page, perPage };
}
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
async function rawRows<T>(query: SqlFragment): Promise<T[]> {
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
return rows ?? [];
}
/**
* Shared list loader for the chat / command / trade log tables. The SQL
* fragments come from a per-table spec while the request handling, search
* branching, count + pagination and row mapping stay in one place.
*/
async function loadLogList<RawRow, Row>(
input: ListParamsInput,
spec: LogListSpec<RawRow, Row>,
): Promise<{
rows: Row[];
total: number;
page: number;
perPage: number;
lastPage: number;
offset: number;
}> {
await requireLogsView();
const rawParams = await input.searchParams;
const { search, page, perPage } = parsePageParams(
rawParams,
input.defaultPerPage ?? 50,
);
const offset = (page - 1) * perPage;
const like = `%${search}%`;
const where = search ? sql`WHERE ${spec.searchWhere(like)}` : sql``;
const [rawRowsResult, countRows] = await Promise.all([
rawRows<RawRow>(
sql`${spec.select} ${spec.from} ${where} ${spec.orderBy} LIMIT ${perPage} OFFSET ${offset}`,
),
rawRows<{ count: bigint }>(
sql`SELECT COUNT(*) AS count ${spec.from} ${where}`,
),
]);
const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage);
const rows = rawRowsResult.map((row, index) =>
spec.mapRow(row, index, pagination.offset),
);
return { rows, ...pagination };
}
export type ChatLogListRow = {
id: number;
username: string;
@@ -39,17 +94,7 @@ export type ChatLogListRow = {
roomId: number;
};
export async function loadChatLogList({
searchParams,
defaultPerPage = 50,
}: ListParamsInput) {
await requireLogsView();
const rawParams = await searchParams;
const { search, page, perPage } = parsePageParams(rawParams, defaultPerPage);
const offset = (page - 1) * perPage;
const like = `%${search}%`;
export function loadChatLogList(input: ListParamsInput) {
type RawRow = {
roomId: number;
message: string;
@@ -57,43 +102,20 @@ export async function loadChatLogList({
username: string | null;
};
const [rawRowsResult, countRows] = search
? await Promise.all([
rawRows<RawRow>(sql`
SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username
FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id
WHERE c.message LIKE ${like} OR u.username LIKE ${like}
ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`),
rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count
FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id
WHERE c.message LIKE ${like} OR u.username LIKE ${like}`),
])
: await Promise.all([
rawRows<RawRow>(sql`
SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username
FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id
ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`),
rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count FROM chatlogs_room`),
]);
const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage);
const rows: ChatLogListRow[] = rawRowsResult.map((row, index) => ({
id: pagination.offset + index + 1,
username: row.username ?? "-",
message: row.message,
timestamp: row.timestamp,
roomId: row.roomId,
}));
return { rows, ...pagination };
return loadLogList<RawRow, ChatLogListRow>(input, {
select: sql`SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username`,
from: sql`FROM chatlogs_room c LEFT JOIN users u ON u.id = c.user_from_id`,
searchWhere: (like) =>
sql`c.message LIKE ${like} OR u.username LIKE ${like}`,
orderBy: sql`ORDER BY c.timestamp DESC`,
mapRow: (row, index, offset) => ({
id: offset + index + 1,
username: row.username ?? "-",
message: row.message,
timestamp: row.timestamp,
roomId: row.roomId,
}),
});
}
export type CommandLogListRow = {
@@ -104,17 +126,7 @@ export type CommandLogListRow = {
timestamp: number;
};
export async function loadCommandLogList({
searchParams,
defaultPerPage = 50,
}: ListParamsInput) {
await requireLogsView();
const rawParams = await searchParams;
const { search, page, perPage } = parsePageParams(rawParams, defaultPerPage);
const offset = (page - 1) * perPage;
const like = `%${search}%`;
export function loadCommandLogList(input: ListParamsInput) {
type RawRow = {
command: string;
params: string;
@@ -122,47 +134,20 @@ export async function loadCommandLogList({
username: string | null;
};
const [rawRowsResult, countRows] = search
? await Promise.all([
rawRows<RawRow>(sql`
SELECT c.command, c.params, c.timestamp, u.username AS username
FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id
WHERE c.command LIKE ${like}
OR c.params LIKE ${like}
OR u.username LIKE ${like}
ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`),
rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count
FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id
WHERE c.command LIKE ${like}
OR c.params LIKE ${like}
OR u.username LIKE ${like}`),
])
: await Promise.all([
rawRows<RawRow>(sql`
SELECT c.command, c.params, c.timestamp, u.username AS username
FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id
ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`),
rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count FROM commandlogs`),
]);
const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage);
const rows: CommandLogListRow[] = rawRowsResult.map((row, index) => ({
id: pagination.offset + index + 1,
username: row.username ?? "-",
command: row.command,
params: row.params,
timestamp: row.timestamp,
}));
return { rows, ...pagination };
return loadLogList<RawRow, CommandLogListRow>(input, {
select: sql`SELECT c.command, c.params, c.timestamp, u.username AS username`,
from: sql`FROM commandlogs c LEFT JOIN users u ON u.id = c.user_id`,
searchWhere: (like) =>
sql`c.command LIKE ${like} OR c.params LIKE ${like} OR u.username LIKE ${like}`,
orderBy: sql`ORDER BY c.timestamp DESC`,
mapRow: (row, index, offset) => ({
id: offset + index + 1,
username: row.username ?? "-",
command: row.command,
params: row.params,
timestamp: row.timestamp,
}),
});
}
export type TradeLogListRow = {
@@ -173,17 +158,7 @@ export type TradeLogListRow = {
timestamp: number;
};
export async function loadTradeLogList({
searchParams,
defaultPerPage = 50,
}: ListParamsInput) {
await requireLogsView();
const rawParams = await searchParams;
const { search, page, perPage } = parsePageParams(rawParams, defaultPerPage);
const offset = (page - 1) * perPage;
const like = `%${search}%`;
export function loadTradeLogList(input: ListParamsInput) {
type RawRow = {
id: number;
timestamp: number;
@@ -191,47 +166,19 @@ export async function loadTradeLogList({
userTwoName: string | null;
};
const [rawRowsResult, countRows] = search
? await Promise.all([
rawRows<RawRow>(sql`
SELECT t.id, t.timestamp,
u1.username AS userOneName, u2.username AS userTwoName
FROM room_trade_log t
LEFT JOIN users u1 ON u1.id = t.user_one_id
LEFT JOIN users u2 ON u2.id = t.user_two_id
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}
ORDER BY t.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`),
rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count
FROM room_trade_log t
LEFT JOIN users u1 ON u1.id = t.user_one_id
LEFT JOIN users u2 ON u2.id = t.user_two_id
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`),
])
: await Promise.all([
rawRows<RawRow>(sql`
SELECT t.id, t.timestamp,
u1.username AS userOneName, u2.username AS userTwoName
FROM room_trade_log t
LEFT JOIN users u1 ON u1.id = t.user_one_id
LEFT JOIN users u2 ON u2.id = t.user_two_id
ORDER BY t.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`),
rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count FROM room_trade_log`),
]);
const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage);
const rows: TradeLogListRow[] = rawRowsResult.map((row) => ({
id: row.id,
userOneName: row.userOneName ?? "-",
userTwoName: row.userTwoName ?? "-",
// room_trade_log only stores completed trades; no status column exists
status: "completed",
timestamp: row.timestamp,
}));
return { rows, ...pagination };
return loadLogList<RawRow, TradeLogListRow>(input, {
select: sql`SELECT t.id, t.timestamp, u1.username AS userOneName, u2.username AS userTwoName`,
from: sql`FROM room_trade_log t LEFT JOIN users u1 ON u1.id = t.user_one_id LEFT JOIN users u2 ON u2.id = t.user_two_id`,
searchWhere: (like) =>
sql`u1.username LIKE ${like} OR u2.username LIKE ${like}`,
orderBy: sql`ORDER BY t.timestamp DESC`,
mapRow: (row) => ({
id: row.id,
userOneName: row.userOneName ?? "-",
userTwoName: row.userTwoName ?? "-",
// room_trade_log only stores completed trades; no status column exists
status: "completed",
timestamp: row.timestamp,
}),
});
}