1
0

refactor: 统一 target name/description 可空语义,前端展示 fallback 到 id

- schema: name/description 允许省略或显式 null,TypeBox Union([Null, String])
- 类型: RawTargetConfig/ResolvedTargetBase/子类型/StoredTarget/TargetStatus name 改为 string | null
- checker resolve: name: t.name ?? null,不再 fallback 到 id
- 语义校验: 拒绝空字符串和纯空白 name
- SQLite: targets.name 列改为可空 TEXT
- 前端: 新增 getTargetDisplayName(target) 展示 name ?? id
- 测试: 覆盖 name/description null 全场景,查找改为按 id
- 文档: 更新 README/DEVELOPMENT 和 6 个 openspec specs
This commit is contained in:
2026-05-17 20:12:39 +08:00
parent f7193e98ff
commit 31fd3a2a43
29 changed files with 382 additions and 119 deletions

View File

@@ -121,4 +121,10 @@ describe("TargetDetailDrawer", () => {
const dragLine = wrapper.querySelector('[style*="col-resize"]');
expect(dragLine).not.toBeNull();
});
test("name 为 null 时标题显示 id", () => {
const nullNameTarget = { ...target, name: null };
render(<TargetDetailDrawer {...defaultProps} target={nullNameTarget} />);
expect(document.body.textContent).toContain("1");
});
});

View File

@@ -156,4 +156,28 @@ describe("createTargetTableColumns", () => {
expect(nameColumn.sorter).toBeUndefined();
expect(nameColumn.sortType).toBeUndefined();
});
test("名称列 name 为 null 时显示 id", () => {
const nameColumn = getColumn(createTargetTableColumns(["http"]), "name");
const renderCell = nameColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => string;
const result = renderCell({
col: nameColumn,
colIndex: 1,
row: makeTarget({ id: "my-api", name: null }),
rowIndex: 0,
});
expect(result).toBe("my-api");
});
test("名称列 name 有值时显示 name", () => {
const nameColumn = getColumn(createTargetTableColumns(["http"]), "name");
const renderCell = nameColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => string;
const result = renderCell({
col: nameColumn,
colIndex: 1,
row: makeTarget({ id: "my-api", name: "我的 API" }),
rowIndex: 0,
});
expect(result).toBe("我的 API");
});
});

View File

@@ -0,0 +1,32 @@
import { describe, expect, test } from "bun:test";
import type { TargetStatus } from "../../../src/shared/api";
import { getTargetDisplayName } from "../../../src/web/utils/target";
function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
return {
currentStreak: null,
description: null,
group: "default",
id: "api-health",
interval: "30s",
latestCheck: null,
name: null,
recentSamples: [],
stats: { availability: 100, downChecks: 0, totalChecks: 0, upChecks: 0 },
target: "https://example.com",
type: "http",
...overrides,
};
}
describe("getTargetDisplayName", () => {
test("name 为 null 时返回 id", () => {
expect(getTargetDisplayName(makeTarget())).toBe("api-health");
});
test("name 有值时返回 name", () => {
expect(getTargetDisplayName(makeTarget({ name: "我的 API" }))).toBe("我的 API");
});
});