import { fieldRefKind, toEntityId, toEntityIds, } from "@/data/entity-refs.client"; import { type ComponentImpact, impactDisplay, isComponentImpactList, } from "@/data/status-report-updates.client"; import { cn } from "@/lib/utils"; import { EntityRef, EntityRefList } from "./entity-ref/entity-ref"; import { StatusBadge } from "./status-badge"; /** * Field-level diff row used by the audit-log "CHANGES" view and the * chat tool draft/result cards. * * - Provide both `before` and `after` for an update (renders both lines, * strikethrough on the removed value). * - Provide only `after` for a create (renders a single `+ value` line). * - Provide only `before` for a delete (renders a single `- value` line). * * Use `"before" in row` / `"after" in row` to distinguish "absent" from * "explicitly null" — null/undefined render as `—`. */ export type ChangeRow = { field: string; before?: unknown; after?: unknown; }; export function ChangesTable({ changes }: { changes: ChangeRow[] }) { if (changes.length === 0) return null; return (
{changes.map((change, idx) => (
0 && "border-t", )} >
{change.field}
{"before" in change ? ( ) : null} {"after" in change ? ( ) : null}
))}
); } function DiffLine({ kind, field, value, }: { kind: "added" | "removed"; field: string; value: unknown; }) { const sign = kind === "added" ? "+" : "-"; return (
{sign} {/* Refs and badges are inline boxes; the removed strikethrough has to be set on them. */}
); } function DiffValue({ field, value }: { field: string; value: unknown }) { const kind = fieldRefKind(field); if (kind === "componentImpacts") { if (isComponentImpactList(value) && value.length > 0) { return ; } } else if (kind) { // Strings stay text: a diff value of "4" is not a reference. const id = typeof value === "number" ? toEntityId(value) : undefined; if (id) return ; const ids = toEntityIds(value); if (ids) return ; } return formatValue(value); } function ImpactList({ impacts }: { impacts: ComponentImpact[] }) { return ( {impacts.map(({ pageComponentId, impact }) => { const display = impactDisplay(impact); return ( → {display.label} ); })} ); } const HIDDEN_FIELDS = new Set([ "workspaceId", "createdAt", "updatedAt", "deletedAt", ]); /** Update: rows from `changedFields`. Create: keys of `after`. Delete: keys of `before`. */ export function buildAuditLogChangeRows(input: { before: Record | null | undefined; after: Record | null | undefined; changedFields: string[] | null | undefined; }): ChangeRow[] { const { before, after, changedFields } = input; if (before && after) { if (!changedFields?.length) return []; return changedFields .filter((field) => !HIDDEN_FIELDS.has(field)) .map((field) => ({ field, before: before[field], after: after[field], })); } if (after) { return Object.keys(after) .filter((field) => !HIDDEN_FIELDS.has(field)) .map((field) => ({ field, after: after[field] })); } if (before) { return Object.keys(before) .filter((field) => !HIDDEN_FIELDS.has(field)) .map((field) => ({ field, before: before[field] })); } return []; } export function formatValue(value: unknown): string { if (value === null || value === undefined) return "—"; if (value instanceof Date) return value.toISOString(); if (typeof value === "string") return value === "" ? "—" : value; if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (Array.isArray(value) && value.length === 0) return "—"; return JSON.stringify(value); }