feat: update activity feed to include various event types and improve messaging

This commit is contained in:
Abhimanyu Saharan
2026-02-12 15:21:41 +05:30
parent 284f03f868
commit c73103d5c9
10 changed files with 1726 additions and 335 deletions

View File

@@ -140,9 +140,20 @@ const formatRubricTooltipValue = (
);
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const payloadAtPath = (payload: Approval["payload"], path: string[]) => {
let current: unknown = payload;
for (const key of path) {
if (!isRecord(current)) return null;
current = current[key];
}
return current ?? null;
};
const payloadValue = (payload: Approval["payload"], key: string) => {
if (!payload) return null;
const value = payload[key as keyof typeof payload];
const value = payloadAtPath(payload, [key]);
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
@@ -150,12 +161,59 @@ const payloadValue = (payload: Approval["payload"], key: string) => {
};
const payloadValues = (payload: Approval["payload"], key: string) => {
if (!payload) return [];
const value = payload[key as keyof typeof payload];
const value = payloadAtPath(payload, [key]);
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string");
};
const payloadNestedValue = (payload: Approval["payload"], path: string[]) => {
const value = payloadAtPath(payload, path);
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
return null;
};
const payloadNestedValues = (payload: Approval["payload"], path: string[]) => {
const value = payloadAtPath(payload, path);
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string");
};
const payloadFirstLinkedTaskValue = (
payload: Approval["payload"],
key: "title" | "description",
) => {
const tasks = payloadAtPath(payload, ["linked_request", "tasks"]);
if (!Array.isArray(tasks)) return null;
for (const task of tasks) {
if (!isRecord(task)) continue;
const value = task[key];
if (typeof value === "string" && value.trim()) {
return value;
}
}
return null;
};
const normalizeRubricScores = (raw: unknown): Record<string, number> => {
if (!isRecord(raw)) return {};
const entries = Object.entries(raw).flatMap(([key, value]) => {
const numeric =
typeof value === "number"
? value
: typeof value === "string"
? Number(value)
: Number.NaN;
if (!Number.isFinite(numeric)) return [];
return [[key, numeric] as const];
});
return Object.fromEntries(entries);
};
const payloadRubricScores = (payload: Approval["payload"]) =>
normalizeRubricScores(payloadAtPath(payload, ["analytics", "rubric_scores"]));
const approvalTaskIds = (approval: Approval) => {
const payload = approval.payload ?? {};
const linkedTaskIds = (approval as Approval & { task_ids?: string[] | null })
@@ -170,6 +228,10 @@ const approvalTaskIds = (approval: Approval) => {
...payloadValues(payload, "task_ids"),
...payloadValues(payload, "taskIds"),
...payloadValues(payload, "taskIDs"),
...payloadNestedValues(payload, ["linked_request", "task_ids"]),
...payloadNestedValues(payload, ["linked_request", "taskIds"]),
...payloadNestedValues(payload, ["linkedRequest", "task_ids"]),
...payloadNestedValues(payload, ["linkedRequest", "taskIds"]),
...(singleTaskId ? [singleTaskId] : []),
];
return [...new Set(merged)];
@@ -181,10 +243,20 @@ const approvalSummary = (approval: Approval, boardLabel?: string | null) => {
const taskId = taskIds[0] ?? null;
const assignedAgentId =
payloadValue(payload, "assigned_agent_id") ??
payloadValue(payload, "assignedAgentId");
const reason = payloadValue(payload, "reason");
const title = payloadValue(payload, "title");
const description = payloadValue(payload, "description");
payloadValue(payload, "assignedAgentId") ??
payloadNestedValue(payload, ["assignment", "agent_id"]) ??
payloadNestedValue(payload, ["assignment", "agentId"]);
const reason =
payloadValue(payload, "reason") ??
payloadNestedValue(payload, ["decision", "reason"]);
const title =
payloadValue(payload, "title") ??
payloadNestedValue(payload, ["task", "title"]) ??
payloadFirstLinkedTaskValue(payload, "title");
const description =
payloadValue(payload, "description") ??
payloadNestedValue(payload, ["task", "description"]) ??
payloadFirstLinkedTaskValue(payload, "description");
const role = payloadValue(payload, "role");
const isAssign = approval.action_type.includes("assign");
const rows: Array<{ label: string; value: string }> = [];
@@ -517,14 +589,20 @@ export function BoardApprovalsPanel({
if (normalized === "assignee") return false;
return true;
});
const rubricEntries = Object.entries(
selectedApproval.rubric_scores ?? {},
).map(([key, value]) => ({
label: key
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase()),
value,
}));
const rubricScoreSource =
Object.keys(
normalizeRubricScores(selectedApproval.rubric_scores),
).length > 0
? normalizeRubricScores(selectedApproval.rubric_scores)
: payloadRubricScores(selectedApproval.payload);
const rubricEntries = Object.entries(rubricScoreSource).map(
([key, value]) => ({
label: key
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase()),
value,
}),
);
const rubricTotal = rubricEntries.reduce(
(total, entry) => total + entry.value,
0,

View File

@@ -55,9 +55,9 @@ describe("ActivityFeed", () => {
/>,
);
expect(screen.getByText("Waiting for new comments…")).toBeInTheDocument();
expect(screen.getByText("Waiting for new activity…")).toBeInTheDocument();
expect(
screen.getByText("When agents post updates, they will show up here."),
screen.getByText("When updates happen, they will show up here."),
).toBeInTheDocument();
});

View File

@@ -34,10 +34,10 @@ export function ActivityFeed<TItem extends FeedItem>({
return (
<div className="rounded-xl border border-slate-200 bg-white p-10 text-center shadow-sm">
<p className="text-sm font-medium text-slate-900">
Waiting for new comments
Waiting for new activity
</p>
<p className="mt-1 text-sm text-slate-500">
When agents post updates, they will show up here.
When updates happen, they will show up here.
</p>
</div>
);