feat: add approval-task links model and related functionality for task associations

This commit is contained in:
Abhimanyu Saharan
2026-02-11 20:27:04 +05:30
parent 3dfdfa3c3e
commit af8a263c27
19 changed files with 870 additions and 129 deletions

View File

@@ -1113,11 +1113,17 @@ export default function BoardDetailPage() {
try {
const payload = JSON.parse(data) as {
approval?: ApprovalRead;
task_counts?: {
task_id?: string;
approvals_count?: number;
approvals_pending_count?: number;
};
task_counts?:
| {
task_id?: string;
approvals_count?: number;
approvals_pending_count?: number;
}
| Array<{
task_id?: string;
approvals_count?: number;
approvals_pending_count?: number;
}>;
pending_approvals_count?: number;
};
if (payload.approval) {
@@ -1137,23 +1143,30 @@ export default function BoardDetailPage() {
return next;
});
}
if (payload.task_counts?.task_id) {
const taskId = payload.task_counts.task_id;
const taskCounts = Array.isArray(payload.task_counts)
? payload.task_counts
: payload.task_counts
? [payload.task_counts]
: [];
if (taskCounts.length > 0) {
setTasks((prev) => {
const index = prev.findIndex((task) => task.id === taskId);
if (index === -1) return prev;
const next = [...prev];
const current = next[index];
next[index] = {
...current,
approvals_count:
payload.task_counts?.approvals_count ??
current.approvals_count,
approvals_pending_count:
payload.task_counts?.approvals_pending_count ??
current.approvals_pending_count,
};
return next;
const countsByTaskId = new Map(
taskCounts
.filter((row) => Boolean(row.task_id))
.map((row) => [row.task_id as string, row]),
);
return prev.map((task) => {
const counts = countsByTaskId.get(task.id);
if (!counts) return task;
return {
...task,
approvals_count:
counts.approvals_count ?? task.approvals_count,
approvals_pending_count:
counts.approvals_pending_count ??
task.approvals_pending_count,
};
});
});
}
} catch {
@@ -1721,7 +1734,37 @@ export default function BoardDetailPage() {
const taskApprovals = useMemo(() => {
if (!selectedTask) return [];
const taskId = selectedTask.id;
return approvals.filter((approval) => approval.task_id === taskId);
const taskIdsForApproval = (approval: Approval) => {
const payload = approval.payload ?? {};
const payloadValue = (key: string) => {
const value = (payload as Record<string, unknown>)[key];
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
return null;
};
const payloadArray = (key: string) => {
const value = (payload as Record<string, unknown>)[key];
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string");
};
const linkedTaskIds = (approval as Approval & { task_ids?: string[] | null })
.task_ids;
const singleTaskId =
approval.task_id ??
payloadValue("task_id") ??
payloadValue("taskId") ??
payloadValue("taskID");
const merged = [
...(Array.isArray(linkedTaskIds) ? linkedTaskIds : []),
...payloadArray("task_ids"),
...payloadArray("taskIds"),
...payloadArray("taskIDs"),
...(singleTaskId ? [singleTaskId] : []),
];
return [...new Set(merged)];
};
return approvals.filter((approval) => taskIdsForApproval(approval).includes(taskId));
}, [approvals, selectedTask]);
const workingAgentIds = useMemo(() => {
@@ -2169,13 +2212,45 @@ export default function BoardDetailPage() {
return null;
};
const approvalRows = (approval: Approval) => {
const approvalPayloadValues = (payload: Approval["payload"], key: string) => {
if (!payload || typeof payload !== "object") return [];
const value = (payload as Record<string, unknown>)[key];
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string");
};
const approvalTaskIds = (approval: Approval) => {
const payload = approval.payload ?? {};
const taskId =
const linkedTaskIds = (approval as Approval & { task_ids?: string[] | null })
.task_ids;
const singleTaskId =
approval.task_id ??
approvalPayloadValue(payload, "task_id") ??
approvalPayloadValue(payload, "taskId") ??
approvalPayloadValue(payload, "taskID");
const manyTaskIds = [
...approvalPayloadValues(payload, "task_ids"),
...approvalPayloadValues(payload, "taskIds"),
...approvalPayloadValues(payload, "taskIDs"),
];
const merged = [
...(Array.isArray(linkedTaskIds) ? linkedTaskIds : []),
...manyTaskIds,
...(singleTaskId ? [singleTaskId] : []),
];
const deduped: string[] = [];
const seen = new Set<string>();
merged.forEach((value) => {
if (seen.has(value)) return;
seen.add(value);
deduped.push(value);
});
return deduped;
};
const approvalRows = (approval: Approval) => {
const payload = approval.payload ?? {};
const taskIds = approvalTaskIds(approval);
const assignedAgentId =
approvalPayloadValue(payload, "assigned_agent_id") ??
approvalPayloadValue(payload, "assignedAgentId");
@@ -2183,7 +2258,8 @@ export default function BoardDetailPage() {
const role = approvalPayloadValue(payload, "role");
const isAssign = approval.action_type.includes("assign");
const rows: Array<{ label: string; value: string }> = [];
if (taskId) rows.push({ label: "Task", value: taskId });
if (taskIds.length === 1) rows.push({ label: "Task", value: taskIds[0] });
if (taskIds.length > 1) rows.push({ label: "Tasks", value: taskIds.join(", ") });
if (isAssign) {
rows.push({
label: "Assignee",

View File

@@ -149,13 +149,36 @@ const payloadValue = (payload: Approval["payload"], key: string) => {
return null;
};
const approvalSummary = (approval: Approval, boardLabel?: string | null) => {
const payloadValues = (payload: Approval["payload"], key: string) => {
if (!payload) return [];
const value = payload[key as keyof typeof payload];
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string");
};
const approvalTaskIds = (approval: Approval) => {
const payload = approval.payload ?? {};
const taskId =
const linkedTaskIds = (approval as Approval & { task_ids?: string[] | null })
.task_ids;
const singleTaskId =
approval.task_id ??
payloadValue(payload, "task_id") ??
payloadValue(payload, "taskId") ??
payloadValue(payload, "taskID");
const merged = [
...(Array.isArray(linkedTaskIds) ? linkedTaskIds : []),
...payloadValues(payload, "task_ids"),
...payloadValues(payload, "taskIds"),
...payloadValues(payload, "taskIDs"),
...(singleTaskId ? [singleTaskId] : []),
];
return [...new Set(merged)];
};
const approvalSummary = (approval: Approval, boardLabel?: string | null) => {
const payload = approval.payload ?? {};
const taskIds = approvalTaskIds(approval);
const taskId = taskIds[0] ?? null;
const assignedAgentId =
payloadValue(payload, "assigned_agent_id") ??
payloadValue(payload, "assignedAgentId");
@@ -166,7 +189,8 @@ const approvalSummary = (approval: Approval, boardLabel?: string | null) => {
const isAssign = approval.action_type.includes("assign");
const rows: Array<{ label: string; value: string }> = [];
if (boardLabel) rows.push({ label: "Board", value: boardLabel });
if (taskId) rows.push({ label: "Task", value: taskId });
if (taskIds.length === 1) rows.push({ label: "Task", value: taskIds[0] });
if (taskIds.length > 1) rows.push({ label: "Tasks", value: taskIds.join(", ") });
if (isAssign) {
rows.push({
label: "Assignee",