fix(api): normalize null lists to [] (backend serializes empty as null)
The Go backend serializes empty slices as JSON null in several places
(e.g. {count:0, topics:null} when no rows match). The SPA assumed
arrays and crashed on .length:
TypeError: Cannot read properties of null (reading 'length')
at Hh (TopicList.tsx:64:45)
Fix in api.ts so every list-returning helper normalizes null → [] once,
centrally. Three call sites covered:
- listTopics — topics field
- listArguments — arguments field
- getAgentSummary — recent_topics field
Components stay as-is; nullable handling moves out of UI code where it
belongs in the data layer. getTopic preserves camps:null vs camps:[]
distinction (null = not yet allocated, [] would be allocated-with-zero
which the allocator contract forbids — keep them meaningfully distinct).
No backend change. Tested on prod with empty topics — list page now
renders 'no topics match this filter' instead of crashing.
This commit is contained in:
45
src/api.ts
45
src/api.ts
@@ -64,7 +64,12 @@ async function request<T>(
|
||||
// --------------------------------------------------------------------
|
||||
// Topics
|
||||
|
||||
export function listTopics(filter: {
|
||||
// Note on null vs []: the Go backend serializes empty slices as JSON
|
||||
// null (`json:",omitempty"`-ish behavior in places), so all list-returning
|
||||
// endpoints can return e.g. {count:0, topics:null}. Normalize to [] here
|
||||
// so consumers can do .length / .map without per-call guards.
|
||||
|
||||
export async function listTopics(filter: {
|
||||
status?: TopicStatus;
|
||||
visibility?: Visibility;
|
||||
limit?: number;
|
||||
@@ -77,23 +82,35 @@ export function listTopics(filter: {
|
||||
if (filter.limit) qs.set('limit', String(filter.limit));
|
||||
if (filter.offset) qs.set('offset', String(filter.offset));
|
||||
const suffix = qs.toString() ? `?${qs}` : '';
|
||||
return request('GET', `/topics${suffix}`, { signal: filter.signal });
|
||||
const r = await request<{ topics: Topic[] | null; count: number }>(
|
||||
'GET',
|
||||
`/topics${suffix}`,
|
||||
{ signal: filter.signal },
|
||||
);
|
||||
return { topics: r.topics ?? [], count: r.count ?? 0 };
|
||||
}
|
||||
|
||||
export function getTopic(
|
||||
export async function getTopic(
|
||||
id: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TopicDetail> {
|
||||
return request('GET', `/topics/${encodeURIComponent(id)}`, { signal });
|
||||
const t = await request<TopicDetail>('GET', `/topics/${encodeURIComponent(id)}`, { signal });
|
||||
// camps is nullable pre-allocation; keep null vs [] meaningful — null
|
||||
// means "not yet allocated" (UI shows '—'), [] would mean "allocated
|
||||
// but no rows" (impossible by allocator contract, but distinct).
|
||||
return t;
|
||||
}
|
||||
|
||||
export function listArguments(
|
||||
export async function listArguments(
|
||||
topicId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ arguments: Argument[]; count: number }> {
|
||||
return request('GET', `/topics/${encodeURIComponent(topicId)}/arguments`, {
|
||||
signal,
|
||||
});
|
||||
const r = await request<{ arguments: Argument[] | null; count: number }>(
|
||||
'GET',
|
||||
`/topics/${encodeURIComponent(topicId)}/arguments`,
|
||||
{ signal },
|
||||
);
|
||||
return { arguments: r.arguments ?? [], count: r.count ?? 0 };
|
||||
}
|
||||
|
||||
export function getVerdict(
|
||||
@@ -112,14 +129,16 @@ export function getVerdict(
|
||||
// --------------------------------------------------------------------
|
||||
// Admin
|
||||
|
||||
export function getAgentSummary(
|
||||
export async function getAgentSummary(
|
||||
agentId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AgentSummary> {
|
||||
return request('GET', `/admin/agents/${encodeURIComponent(agentId)}`, {
|
||||
admin: true,
|
||||
signal,
|
||||
});
|
||||
const r = await request<AgentSummary & { recent_topics: AgentSummary['recent_topics'] | null }>(
|
||||
'GET',
|
||||
`/admin/agents/${encodeURIComponent(agentId)}`,
|
||||
{ admin: true, signal },
|
||||
);
|
||||
return { ...r, recent_topics: r.recent_topics ?? [] };
|
||||
}
|
||||
|
||||
export { HttpError };
|
||||
|
||||
Reference in New Issue
Block a user