修复 PR 232 后台表查询评审问题
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled

后台筛选要求 eq/ne 必须提供标量 value,缺失时返回 400。

统一表单和标题区查询入口的筛选完整性校验。

行内交互控件键盘事件不再冒泡触发行详情。

空字段详情复制给出明确失败反馈。

补充后端与后台页面回归测试。

同步更新后台表查询契约说明与运维文档。
This commit is contained in:
2026-09-01 11:35:19 +08:00
parent 98e43b7957
commit 3a3df3aabb
5 changed files with 132 additions and 6 deletions
@@ -72,6 +72,7 @@ const referralRows = [
];
beforeEach(() => {
vi.clearAllMocks();
window.location.hash = '#tables?table=profile_referral_relation';
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
fetchErrors: [],
@@ -372,6 +373,21 @@ test('数据库用户字段显示查看按钮且点击不会打开行详情', as
expect(screen.queryByRole('dialog')).toBeNull();
});
test('数据库用户查看按钮上的键盘操作不会同时打开行详情', async () => {
const user = userEvent.setup();
render(
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const userButton = await screen.findByRole('button', {
name: '查看用户 u-b',
});
userButton.focus();
await user.keyboard('{Enter}');
expect(screen.queryByRole('dialog')).toBeNull();
});
test('数据库用户字段识别会排除后台操作者与合成邀请码字段', () => {
expect(
resolveAdminDatabaseUserReference('profile_wallet', 'owner_user_id', 'u-1'),
@@ -528,6 +544,22 @@ test('后台表查询页会提示未完成的启用条件而不是静默忽略',
expect(rowsRequest).not.toHaveBeenCalled();
});
test('后台表查询页顶部查询按钮会提示未完成的启用条件而不是静默忽略', async () => {
const user = userEvent.setup();
const rowsRequest = vi.mocked(getAdminDatabaseTableRows);
render(
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByText('u-b');
rowsRequest.mockClear();
await user.click(screen.getByRole('button', { name: '添加条件' }));
await user.click(getHeadingQueryButton());
expect(await screen.findByText('条件 1 未完成,请补充后再查询')).toBeTruthy();
expect(rowsRequest).not.toHaveBeenCalled();
});
test('后台表查询页列头筛选按钮会带字段添加条件', async () => {
const user = userEvent.setup();
render(
@@ -573,11 +605,55 @@ test('后台表查询页行详情仅保留复制操作', async () => {
expect(screen.getAllByRole('button', { name: /^复制/ })).toHaveLength(4);
});
test('后台表查询页空字段详情复制会明确提示无法复制空值', async () => {
const user = userEvent.setup();
const writeText = vi.fn();
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
});
vi.mocked(getAdminDatabaseTableRows).mockResolvedValueOnce({
columns: ['id', 'deleted_at'],
limit: 100,
page: 1,
rows: [
{
cells: {
id: 'row-null',
deleted_at: null,
},
raw: ['row-null', null],
},
],
scannedCount: 1,
scanLimit: 50000,
scanLimitReached: false,
tableName: 'profile_referral_relation',
totalMatched: 1,
totalReturned: 1,
});
render(
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByText('row-null');
await user.click(screen.getByText('row-null').closest('tr')!);
await user.click(screen.getByRole('button', { name: '复制删除时间' }));
expect(screen.getByRole('alert').textContent).toBe('该字段为空,无法复制');
expect(writeText).not.toHaveBeenCalled();
});
function getFormSubmitQueryButton() {
const buttons = screen.getAllByRole('button', { name: '查询' });
return buttons[buttons.length - 1]!;
}
function getHeadingQueryButton() {
const buttons = screen.getAllByRole('button', { name: '查询' });
return buttons[0]!;
}
function readFirstColumnValues(container: HTMLElement) {
return Array.from(container.querySelectorAll('tbody tr')).map(
(row) => row.querySelector('td')?.textContent?.trim() ?? '',
@@ -305,6 +305,15 @@ export function AdminDatabaseTablesPage({
void refreshRows(tableName, { page: 1 });
}
function handleQueryRows() {
const validationMessage = getDatabaseFilterValidationMessage(conditions);
if (validationMessage) {
setErrorMessage(validationMessage);
return;
}
void refreshRows(tableName, { page: 1 });
}
function handleTableChange(nextTableName: string) {
setSortColumn('');
setSortDirection('asc');
@@ -471,6 +480,9 @@ export function AdminDatabaseTablesPage({
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
if (isInteractiveFormControl(event.target)) {
return;
}
event.preventDefault();
setDetailRow(row);
}
@@ -495,7 +507,15 @@ export function AdminDatabaseTablesPage({
async function handleCopyDetailField(column: string, value: unknown) {
const copiedText = stringifyDetailValue(value);
if (!copiedText) {
if (
value === null ||
typeof value === 'undefined' ||
copiedText.trim() === ''
) {
setCopyToast({
message: '该字段为空,无法复制',
tone: 'error',
});
return;
}
try {
@@ -536,7 +556,7 @@ export function AdminDatabaseTablesPage({
className="admin-primary-button"
disabled={!tableName || isLoadingRows}
type="button"
onClick={() => void refreshRows(tableName, { page: 1 })}
onClick={handleQueryRows}
>
<Search size={17} aria-hidden="true" />
<span>{isLoadingRows ? '查询中' : '查询'}</span>
@@ -1281,6 +1301,13 @@ function focusConditionControl(index: number, target: 'column' | 'value') {
}, 0);
}
function isInteractiveFormControl(target: EventTarget | null) {
return (
target instanceof HTMLElement &&
target.closest('button, a, input, select, textarea') !== null
);
}
function getDatabaseTableHeader(tableName: string): DatabaseTableHeader {
const normalizedName = tableName.trim();
if (!normalizedName) {