Skip to content

Commit d3e3a21

Browse files
fix: FIT-1091: Consistency for the State History view and Comments View (#8964)
Co-authored-by: bmartel <brandonmartel@gmail.com>
1 parent 358fc9f commit d3e3a21

File tree

11 files changed

+282
-156
lines changed

11 files changed

+282
-156
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* StateHistoryTimeline - Reusable timeline component for displaying state history
3+
*/
4+
5+
import { Userpic, cn, Typography } from "@humansignal/ui";
6+
import type { StateHistoryItem } from "../../hooks/useStateHistory";
7+
import { formatStateName, formatTimestamp, formatUserName } from "./utils";
8+
import { getStateVisuals } from "./state-visuals";
9+
10+
export interface StateHistoryTimelineProps {
11+
history: StateHistoryItem[];
12+
}
13+
14+
/**
15+
* Get user initials from triggered_by object
16+
*/
17+
function getUserInitials(
18+
triggeredBy: {
19+
first_name?: string;
20+
last_name?: string;
21+
email?: string;
22+
} | null,
23+
): string {
24+
if (!triggeredBy) return "SY";
25+
26+
const { first_name, last_name, email } = triggeredBy;
27+
28+
if (first_name && last_name) {
29+
return `${first_name.charAt(0)}${last_name.charAt(0)}`.toUpperCase();
30+
}
31+
if (first_name) return first_name.slice(0, 2).toUpperCase();
32+
if (last_name) return last_name.slice(0, 2).toUpperCase();
33+
if (email) return email.slice(0, 2).toUpperCase();
34+
35+
return "SY";
36+
}
37+
38+
export interface TimelineItemProps {
39+
item: StateHistoryItem;
40+
index: number;
41+
isLast: boolean;
42+
}
43+
44+
/**
45+
* Timeline item component for a single state history entry
46+
*/
47+
export function TimelineItem({ item, index, isLast }: TimelineItemProps) {
48+
const isCurrent = index === 0;
49+
const stateLabel = formatStateName(item.state);
50+
const visuals = getStateVisuals(stateLabel);
51+
const StateIcon = visuals.icon;
52+
53+
// Current state (index 0) gets bold/base colors, past states get subtle colors
54+
const bgColor = isCurrent ? visuals.baseBg : visuals.subtleBg;
55+
const iconColor = isCurrent ? visuals.baseIconColor : visuals.subtleIconColor;
56+
57+
// Text color: current state is dark, all past states are subtle
58+
const labelClass = isCurrent ? "text-neutral-content" : "text-neutral-content-subtle";
59+
60+
const userName = formatUserName(item.triggered_by);
61+
const isSystem = userName === "System";
62+
const reason = item.reason;
63+
64+
return (
65+
<div className="flex gap-2 items-start relative">
66+
{/* Timeline connector line - positioned behind icon, extends to next icon */}
67+
{!isLast && (
68+
<div className="absolute w-px bg-neutral-border" style={{ left: "16px", top: "38px", bottom: "4px" }} />
69+
)}
70+
{/* Icon column */}
71+
<div className="flex flex-col items-center shrink-0 pt-0.5">
72+
{/* State icon with circular background - 32px circle with 24px icon */}
73+
<div className="rounded-full size-8 p-1 flex items-center justify-center" style={{ backgroundColor: bgColor }}>
74+
<StateIcon className="w-6 h-6 shrink-0" style={{ color: iconColor }} />
75+
</div>
76+
</div>
77+
78+
{/* Content */}
79+
<div className={cn("flex flex-col gap-0.5 flex-1 min-h-10 justify-center min-w-0", !isLast && "pb-6")}>
80+
{/* State name and optional reason */}
81+
<div className="flex flex-col gap-1">
82+
<Typography variant="label" size="small" className={`${labelClass} truncate`}>
83+
{stateLabel}
84+
</Typography>
85+
{reason && (
86+
<Typography variant="body" size="smaller" className="text-neutral-content-subtler mt-0.5">
87+
{reason}
88+
</Typography>
89+
)}
90+
</div>
91+
92+
{/* Metadata row */}
93+
<div className="flex items-center gap-2 text-neutral-content-subtler">
94+
{/* Author section */}
95+
{!isSystem && (
96+
<>
97+
<div className="flex items-center gap-1 shrink-0">
98+
<Userpic size={20} user={item.triggered_by} username={getUserInitials(item.triggered_by)} />
99+
<Typography variant="body" size="smaller">
100+
{userName}
101+
</Typography>
102+
</div>
103+
{/* Dot separator */}
104+
<div className="size-[3px] rounded-full bg-neutral-content-subtler shrink-0" />
105+
</>
106+
)}
107+
{isSystem && (
108+
<>
109+
<Typography variant="body" size="smaller">
110+
System
111+
</Typography>
112+
{/* Dot separator */}
113+
<div className="size-[3px] rounded-full bg-neutral-content-subtler shrink-0" />
114+
</>
115+
)}
116+
{/* Timestamp */}
117+
<Typography variant="body" size="smaller">
118+
{formatTimestamp(item.created_at)}
119+
</Typography>
120+
</div>
121+
</div>
122+
</div>
123+
);
124+
}
125+
126+
/**
127+
* Timeline component that renders a list of state history items
128+
*/
129+
export function StateHistoryTimeline({ history }: StateHistoryTimelineProps) {
130+
return (
131+
<div className="flex flex-col">
132+
{history.map((item: StateHistoryItem, index: number) => (
133+
<TimelineItem
134+
key={`${item.state}-${item.created_at}-${index}`}
135+
item={item}
136+
index={index}
137+
isLast={index === history.length - 1}
138+
/>
139+
))}
140+
</div>
141+
);
142+
}

web/libs/app-common/src/components/state-chips/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ export { TaskStateChip, type TaskStateChipProps } from "./task-state-chip";
22
export { AnnotationStateChip, type AnnotationStateChipProps } from "./annotation-state-chip";
33
export { ProjectStateChip, type ProjectStateChipProps } from "./project-state-chip";
44
export { StateHistoryPopoverContent, type StateHistoryPopoverContentProps } from "./state-history-popover-content";
5-
export { StateHistoryPopover, type StateHistoryPopoverProps } from "./state-history-popover";
5+
export { StateHistoryTimeline, type StateHistoryTimelineProps, type TimelineItemProps } from "./StateHistoryTimeline";
66
export * from "./utils";
77
export { stateRegistry, StateType, type EntityType, type StateMetadata } from "./state-registry";

web/libs/app-common/src/components/state-chips/state-history-popover-content.tsx

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* StateHistoryPopoverContent - Popover content for displaying state history
2+
* StateHistoryPopoverContent - Popover content for displaying state history as a timeline
33
*/
44

5-
import { Badge, Button, Typography } from "@humansignal/ui";
5+
import { Button, Typography } from "@humansignal/ui";
66
import { IconSync, IconError, IconHistoryRewind, IconCross } from "@humansignal/icons";
77
import { useStateHistory, type StateHistoryItem } from "../../hooks/useStateHistory";
8-
import { getStateColorClass, formatStateName, formatTimestamp, formatUserName } from "./utils";
8+
import { StateHistoryTimeline } from "./StateHistoryTimeline";
99

1010
export interface StateHistoryPopoverContentProps {
1111
entityType: "task" | "annotation" | "project";
@@ -25,14 +25,14 @@ export function StateHistoryPopoverContent({ entityType, entityId, isOpen, onClo
2525

2626
return (
2727
<div
28-
className="flex flex-col w-[320px] max-h-[400px] bg-primary-background rounded-lg shadow-lg"
28+
className="flex flex-col w-[360px] max-h-[400px] bg-neutral-background rounded-lg shadow-lg"
2929
onClick={(e) => e.stopPropagation()}
3030
>
3131
{/* Header */}
3232
<div className="px-4 py-3 border-b border-neutral-border">
3333
<div className="flex items-center justify-between">
3434
<div className="flex items-center gap-2">
35-
<IconHistoryRewind className="w-4 h-4 text-muted-foreground" />
35+
<IconHistoryRewind className="w-4 h-4" />
3636
<Typography variant="body" size="small" className="font-medium text-neutral-foreground">
3737
State History
3838
</Typography>
@@ -96,31 +96,7 @@ export function StateHistoryPopoverContent({ entityType, entityId, isOpen, onClo
9696
</div>
9797
)}
9898

99-
{!isLoading && !isError && history.length > 0 && (
100-
<div className="space-y-3">
101-
{history.map((item: StateHistoryItem, index: number) => {
102-
const reasonText = item.triggered_by ? formatUserName(item.triggered_by) : item.context_data?.reason;
103-
return (
104-
<div
105-
key={index}
106-
className="flex flex-col gap-2 pb-3 border-b border-neutral-border last:border-0 last:pb-0"
107-
>
108-
<div className="flex items-center justify-between">
109-
<Badge className={getStateColorClass(item.state)}>{formatStateName(item.state)}</Badge>
110-
<Typography variant="body" size="smallest" className="text-neutral-content-subtle">
111-
{formatTimestamp(item.created_at)}
112-
</Typography>
113-
</div>
114-
{reasonText && (
115-
<Typography variant="body" size="smallest" className="text-muted-foreground">
116-
{reasonText}
117-
</Typography>
118-
)}
119-
</div>
120-
);
121-
})}
122-
</div>
123-
)}
99+
{!isLoading && !isError && history.length > 0 && <StateHistoryTimeline history={history} />}
124100
</div>
125101
</div>
126102
);

web/libs/app-common/src/components/state-chips/state-history-popover.tsx

Lines changed: 0 additions & 125 deletions
This file was deleted.

0 commit comments

Comments
 (0)