@handled-ai/design-system 0.20.30 → 0.20.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account-contacts-popover.d.ts +1 -1
- package/dist/components/account-contacts-popover.js +25 -20
- package/dist/components/account-contacts-popover.js.map +1 -1
- package/dist/components/email-recipient-field.d.ts +9 -1
- package/dist/components/email-recipient-field.js +30 -4
- package/dist/components/email-recipient-field.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +14 -4
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/suggested-actions.d.ts +2 -1
- package/dist/components/suggested-actions.js.map +1 -1
- package/dist/components/timeline-activity.js +51 -41
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DIUVhipw.d.ts → signal-priority-popover-BmG4WAxT.d.ts} +17 -1
- package/package.json +1 -1
- package/src/components/__tests__/account-contacts-popover.test.tsx +71 -0
- package/src/components/__tests__/email-recipient-field.test.tsx +123 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +72 -0
- package/src/components/__tests__/timeline-activity.test.tsx +16 -0
- package/src/components/account-contacts-popover.tsx +33 -19
- package/src/components/email-recipient-field.tsx +45 -0
- package/src/components/signal-priority-popover.tsx +50 -21
- package/src/components/suggested-actions.tsx +4 -3
- package/src/components/timeline-activity.tsx +7 -1
|
@@ -513,4 +513,127 @@ describe("EmailRecipientField", () => {
|
|
|
513
513
|
// trigger another search.
|
|
514
514
|
expect(secondOnSearch).not.toHaveBeenCalled()
|
|
515
515
|
})
|
|
516
|
+
|
|
517
|
+
// --- Last activity (WIT-1007) ---
|
|
518
|
+
|
|
519
|
+
const activityContact: SuggestedContact = {
|
|
520
|
+
name: "Cara Customer",
|
|
521
|
+
role: "VP Ops",
|
|
522
|
+
email: "cara@example.com",
|
|
523
|
+
confirmed: true,
|
|
524
|
+
lastActivity: { date: "Jun 8, 2026", type: "email", timelineEventId: "evt-1" },
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
it("renders last activity as non-clickable text when no onOpenRecentActivity callback is provided", () => {
|
|
528
|
+
render(
|
|
529
|
+
<EmailRecipientField
|
|
530
|
+
label="To"
|
|
531
|
+
recipients={[]}
|
|
532
|
+
onRecipientsChange={vi.fn()}
|
|
533
|
+
showPicker
|
|
534
|
+
contacts={[activityContact]}
|
|
535
|
+
/>,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
539
|
+
expect(screen.getByText("Jun 8, 2026")).toBeTruthy()
|
|
540
|
+
// No clickable activity button exists.
|
|
541
|
+
expect(screen.queryByRole("button", { name: /Last activity/i })).toBeNull()
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it("renders last activity as non-clickable text when timelineEventId is missing even with a callback", () => {
|
|
545
|
+
const onOpenRecentActivity = vi.fn()
|
|
546
|
+
render(
|
|
547
|
+
<EmailRecipientField
|
|
548
|
+
label="To"
|
|
549
|
+
recipients={[]}
|
|
550
|
+
onRecipientsChange={vi.fn()}
|
|
551
|
+
showPicker
|
|
552
|
+
contacts={[
|
|
553
|
+
{
|
|
554
|
+
...activityContact,
|
|
555
|
+
lastActivity: { date: "Jun 8, 2026", type: "email" },
|
|
556
|
+
},
|
|
557
|
+
]}
|
|
558
|
+
onOpenRecentActivity={onOpenRecentActivity}
|
|
559
|
+
/>,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
563
|
+
expect(screen.getByText("Jun 8, 2026")).toBeTruthy()
|
|
564
|
+
expect(screen.queryByRole("button", { name: /Last activity/i })).toBeNull()
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it("renders last activity as a clickable button only when callback and timelineEventId both exist", () => {
|
|
568
|
+
const onOpenRecentActivity = vi.fn()
|
|
569
|
+
render(
|
|
570
|
+
<EmailRecipientField
|
|
571
|
+
label="To"
|
|
572
|
+
recipients={[]}
|
|
573
|
+
onRecipientsChange={vi.fn()}
|
|
574
|
+
showPicker
|
|
575
|
+
contacts={[activityContact]}
|
|
576
|
+
onOpenRecentActivity={onOpenRecentActivity}
|
|
577
|
+
/>,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
581
|
+
const activityButton = screen.getByRole("button", { name: /Last activity/i })
|
|
582
|
+
expect(activityButton.tagName).toBe("BUTTON")
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it("invokes onOpenRecentActivity with the contact and does NOT add a recipient on activity click", () => {
|
|
586
|
+
const onOpenRecentActivity = vi.fn()
|
|
587
|
+
const onChange = vi.fn()
|
|
588
|
+
render(
|
|
589
|
+
<EmailRecipientField
|
|
590
|
+
label="To"
|
|
591
|
+
recipients={[]}
|
|
592
|
+
onRecipientsChange={onChange}
|
|
593
|
+
showPicker
|
|
594
|
+
contacts={[activityContact]}
|
|
595
|
+
onOpenRecentActivity={onOpenRecentActivity}
|
|
596
|
+
/>,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
600
|
+
fireEvent.click(screen.getByRole("button", { name: /Last activity/i }))
|
|
601
|
+
|
|
602
|
+
expect(onOpenRecentActivity).toHaveBeenCalledTimes(1)
|
|
603
|
+
expect(onOpenRecentActivity).toHaveBeenCalledWith(activityContact)
|
|
604
|
+
// Clicking the activity must not select/add the recipient.
|
|
605
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it("keeps the last-activity button clickable for already-added contacts without adding the recipient", () => {
|
|
609
|
+
const onOpenRecentActivity = vi.fn()
|
|
610
|
+
const onChange = vi.fn()
|
|
611
|
+
render(
|
|
612
|
+
<EmailRecipientField
|
|
613
|
+
label="To"
|
|
614
|
+
recipients={[]}
|
|
615
|
+
onRecipientsChange={onChange}
|
|
616
|
+
showPicker
|
|
617
|
+
contacts={[activityContact]}
|
|
618
|
+
addedEmails={new Set([activityContact.email!.toLowerCase()])}
|
|
619
|
+
onOpenRecentActivity={onOpenRecentActivity}
|
|
620
|
+
/>,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
|
|
624
|
+
|
|
625
|
+
// The row is disabled (already added) but its activity button stays clickable.
|
|
626
|
+
const option = screen
|
|
627
|
+
.getAllByRole("option")
|
|
628
|
+
.find((o) => o.textContent?.includes("Cara Customer"))!
|
|
629
|
+
expect(option.className).toContain("pointer-events-none")
|
|
630
|
+
const activityButton = within(option).getByRole("button", { name: /Last activity/i })
|
|
631
|
+
expect(activityButton.className).toContain("pointer-events-auto")
|
|
632
|
+
|
|
633
|
+
fireEvent.click(activityButton)
|
|
634
|
+
|
|
635
|
+
expect(onOpenRecentActivity).toHaveBeenCalledTimes(1)
|
|
636
|
+
expect(onOpenRecentActivity).toHaveBeenCalledWith(activityContact)
|
|
637
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
638
|
+
})
|
|
516
639
|
})
|
|
@@ -378,4 +378,76 @@ describe("SignalPriorityPopover", () => {
|
|
|
378
378
|
expect(trigger.className).toContain("text-amber-800")
|
|
379
379
|
expect(trigger.textContent).toContain("Medium Priority")
|
|
380
380
|
})
|
|
381
|
+
|
|
382
|
+
// ─── Custom trigger (Task 0) ───────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
it("renders the default button trigger unchanged when no custom trigger is passed", () => {
|
|
385
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
386
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
387
|
+
// Default trigger is a <button> reading "{urgencyLabel} Priority"
|
|
388
|
+
expect(trigger.tagName).toBe("BUTTON")
|
|
389
|
+
expect(trigger.textContent).toContain("High Priority")
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it("renders a custom trigger inside the popover trigger, merging the data-testid, and opens the panel", () => {
|
|
393
|
+
render(
|
|
394
|
+
<SignalPriorityPopover
|
|
395
|
+
{...defaultProps}
|
|
396
|
+
renderTrigger={({ urgencyLabel, open }) => (
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
aria-label={`View ${urgencyLabel} urgency detail`}
|
|
400
|
+
data-open={open}
|
|
401
|
+
>
|
|
402
|
+
bar-glyph
|
|
403
|
+
</button>
|
|
404
|
+
)}
|
|
405
|
+
/>,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
// data-testid is merged onto the custom child element
|
|
409
|
+
const trigger = screen.getByTestId("priority-popover-trigger")
|
|
410
|
+
expect(trigger.tagName).toBe("BUTTON")
|
|
411
|
+
expect(trigger.textContent).toBe("bar-glyph")
|
|
412
|
+
expect(trigger.getAttribute("aria-label")).toBe("View High urgency detail")
|
|
413
|
+
// No default "High Priority" label rendered
|
|
414
|
+
expect(trigger.textContent).not.toContain("High Priority")
|
|
415
|
+
|
|
416
|
+
// Opens the panel
|
|
417
|
+
expect(screen.queryByTestId("priority-popover-content")).toBeNull()
|
|
418
|
+
fireEvent.click(trigger)
|
|
419
|
+
expect(screen.getByTestId("priority-popover-content")).toBeTruthy()
|
|
420
|
+
// open state is forwarded to the render function
|
|
421
|
+
expect(trigger.getAttribute("data-open")).toBe("true")
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// ─── Controlled open state (Task 0) ─────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
it("respects the controlled open prop for visibility", () => {
|
|
427
|
+
const { rerender } = render(
|
|
428
|
+
<SignalPriorityPopover {...defaultProps} open={false} onOpenChange={vi.fn()} />,
|
|
429
|
+
)
|
|
430
|
+
expect(screen.queryByTestId("priority-popover-content")).toBeNull()
|
|
431
|
+
|
|
432
|
+
rerender(<SignalPriorityPopover {...defaultProps} open={true} onOpenChange={vi.fn()} />)
|
|
433
|
+
expect(screen.getByTestId("priority-popover-content")).toBeTruthy()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it("calls onOpenChange and does not self-open when controlled", () => {
|
|
437
|
+
const onOpenChange = vi.fn()
|
|
438
|
+
render(<SignalPriorityPopover {...defaultProps} open={false} onOpenChange={onOpenChange} />)
|
|
439
|
+
|
|
440
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
441
|
+
|
|
442
|
+
// Controlled: open stays driven by the prop (still false), but handler fires
|
|
443
|
+
expect(onOpenChange).toHaveBeenCalledWith(true)
|
|
444
|
+
expect(screen.queryByTestId("priority-popover-content")).toBeNull()
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it("still works uncontrolled when open/onOpenChange are omitted", () => {
|
|
448
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
449
|
+
expect(screen.queryByTestId("priority-popover-content")).toBeNull()
|
|
450
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
451
|
+
expect(screen.getByTestId("priority-popover-content")).toBeTruthy()
|
|
452
|
+
})
|
|
381
453
|
})
|
|
@@ -44,6 +44,22 @@ describe("TimelineActivity", () => {
|
|
|
44
44
|
expect(container.firstElementChild).toHaveAttribute("data-variant", "default")
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
+
it("renders each timeline item wrapper with scroll/focus anchors and test ids", () => {
|
|
48
|
+
const events = [minimal({ id: "evt-a" }), minimal({ id: "evt-b" })]
|
|
49
|
+
const { container } = render(<TimelineActivity events={events} />)
|
|
50
|
+
|
|
51
|
+
for (const id of ["evt-a", "evt-b"]) {
|
|
52
|
+
const wrapper = container.querySelector(`#timeline-event-${id}`)
|
|
53
|
+
expect(wrapper).not.toBeNull()
|
|
54
|
+
expect(wrapper).toHaveAttribute("data-testid", `timeline-event-${id}`)
|
|
55
|
+
expect(wrapper).toHaveAttribute("data-activity-id", id)
|
|
56
|
+
expect(wrapper).toHaveAttribute("tabindex", "-1")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The test id is also reachable via screen for click-through targeting.
|
|
60
|
+
expect(screen.getByTestId("timeline-event-evt-a")).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
|
|
47
63
|
it("marks the root and interactive cards with the case-panel variant", () => {
|
|
48
64
|
const event = minimal({
|
|
49
65
|
isInteractive: true,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Clock,
|
|
6
6
|
ExternalLink,
|
|
7
7
|
} from "lucide-react"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
8
9
|
import type { SuggestedContact, SuggestedActionsIconMap } from "./suggested-actions"
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -37,7 +38,7 @@ export interface AccountContactsPopoverProps {
|
|
|
37
38
|
/** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */
|
|
38
39
|
onSelectSwitch?: (contact: SuggestedContact) => void
|
|
39
40
|
onViewAll?: () => void
|
|
40
|
-
onOpenRecentActivity?: () => void
|
|
41
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void
|
|
41
42
|
trigger: React.ReactNode
|
|
42
43
|
iconMap?: SuggestedActionsIconMap
|
|
43
44
|
}
|
|
@@ -117,24 +118,37 @@ export function AccountContactsPopover({
|
|
|
117
118
|
<div className="truncate text-xs text-muted-foreground leading-tight">
|
|
118
119
|
{c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? ""}
|
|
119
120
|
</div>
|
|
120
|
-
{c.lastActivity && (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
{c.lastActivity && (() => {
|
|
122
|
+
const activityContent = (
|
|
123
|
+
<>
|
|
124
|
+
<Clock className="w-3 h-3 shrink-0" />
|
|
125
|
+
<span className="shrink-0 font-medium">Last activity</span>
|
|
126
|
+
<span className="shrink-0 text-muted-foreground/60">·</span>
|
|
127
|
+
<span className="shrink-0">{c.lastActivity.date}</span>
|
|
128
|
+
<span className="shrink-0 text-muted-foreground/60">·</span>
|
|
129
|
+
<span className="truncate capitalize">{c.lastActivity.type}</span>
|
|
130
|
+
</>
|
|
131
|
+
)
|
|
132
|
+
const chipBaseClass =
|
|
133
|
+
"mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground"
|
|
134
|
+
return onOpenRecentActivity && c.lastActivity.timelineEventId ? (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={(e) => {
|
|
138
|
+
e.stopPropagation()
|
|
139
|
+
onOpenRecentActivity(c)
|
|
140
|
+
setOpen(false)
|
|
141
|
+
}}
|
|
142
|
+
className={cn(chipBaseClass, "hover:text-foreground hover:bg-muted/50 transition-colors")}
|
|
143
|
+
>
|
|
144
|
+
{activityContent}
|
|
145
|
+
</button>
|
|
146
|
+
) : (
|
|
147
|
+
<div className={chipBaseClass}>
|
|
148
|
+
{activityContent}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
})()}
|
|
138
152
|
</div>
|
|
139
153
|
<div className="ml-2 flex items-center gap-1.5 shrink-0">
|
|
140
154
|
{resolvedDefaultSelectLabel && (
|
|
@@ -4,6 +4,7 @@ import * as React from "react"
|
|
|
4
4
|
import { Popover as PopoverPrimitive } from "radix-ui"
|
|
5
5
|
import {
|
|
6
6
|
ChevronDown,
|
|
7
|
+
Clock,
|
|
7
8
|
CornerDownLeft,
|
|
8
9
|
Plus,
|
|
9
10
|
Search,
|
|
@@ -63,6 +64,14 @@ export interface EmailRecipientFieldProps {
|
|
|
63
64
|
onSearch?: (query: string) => void
|
|
64
65
|
/** Shows a "Searching contacts..." indicator while async results load. */
|
|
65
66
|
searchLoading?: boolean
|
|
67
|
+
/**
|
|
68
|
+
* Opens the recent activity for a contact in the Account Panel timeline.
|
|
69
|
+
* When provided AND the contact's `lastActivity.timelineEventId` exists, the
|
|
70
|
+
* last-activity line renders as a button that calls this callback (and stops
|
|
71
|
+
* propagation so it never adds/selects the recipient). Otherwise the line
|
|
72
|
+
* renders as non-clickable text.
|
|
73
|
+
*/
|
|
74
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
function RecipientChipPill({
|
|
@@ -146,6 +155,7 @@ function ContactPickerContents({
|
|
|
146
155
|
onAddEmail,
|
|
147
156
|
onSearch,
|
|
148
157
|
searchLoading = false,
|
|
158
|
+
onOpenRecentActivity,
|
|
149
159
|
}: {
|
|
150
160
|
contacts: SuggestedContact[]
|
|
151
161
|
addedEmails: Set<string>
|
|
@@ -153,6 +163,7 @@ function ContactPickerContents({
|
|
|
153
163
|
onAddEmail: (email: string) => void
|
|
154
164
|
onSearch?: (query: string) => void
|
|
155
165
|
searchLoading?: boolean
|
|
166
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void
|
|
156
167
|
}) {
|
|
157
168
|
const [query, setQuery] = React.useState("")
|
|
158
169
|
|
|
@@ -279,6 +290,38 @@ function ContactPickerContents({
|
|
|
279
290
|
{email}
|
|
280
291
|
</div>
|
|
281
292
|
) : null}
|
|
293
|
+
{contact.lastActivity ? (() => {
|
|
294
|
+
const activityContent = (
|
|
295
|
+
<>
|
|
296
|
+
<Clock className="size-3 shrink-0" />
|
|
297
|
+
<span className="shrink-0">Last activity</span>
|
|
298
|
+
<span className="shrink-0 text-muted-foreground/60">·</span>
|
|
299
|
+
<span className="shrink-0">{contact.lastActivity.date}</span>
|
|
300
|
+
<span className="shrink-0 text-muted-foreground/60">·</span>
|
|
301
|
+
<span className="truncate capitalize">{contact.lastActivity.type}</span>
|
|
302
|
+
</>
|
|
303
|
+
)
|
|
304
|
+
return onOpenRecentActivity && contact.lastActivity.timelineEventId ? (
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
onClick={(event) => {
|
|
308
|
+
event.stopPropagation()
|
|
309
|
+
onOpenRecentActivity(contact)
|
|
310
|
+
}}
|
|
311
|
+
// `pointer-events-auto` keeps the activity button clickable even
|
|
312
|
+
// when the parent row is disabled (no email / already added) and
|
|
313
|
+
// gets `pointer-events-none` — the timeline activity is still
|
|
314
|
+
// openable for those contacts.
|
|
315
|
+
className="mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground hover:text-foreground transition-colors pointer-events-auto"
|
|
316
|
+
>
|
|
317
|
+
{activityContent}
|
|
318
|
+
</button>
|
|
319
|
+
) : (
|
|
320
|
+
<div className="mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground">
|
|
321
|
+
{activityContent}
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
})() : null}
|
|
282
325
|
</div>
|
|
283
326
|
{alreadyAdded ? (
|
|
284
327
|
<span className="shrink-0 text-[10.5px] font-medium text-muted-foreground">
|
|
@@ -318,6 +361,7 @@ export function EmailRecipientField({
|
|
|
318
361
|
contactToRecipient,
|
|
319
362
|
onSearch,
|
|
320
363
|
searchLoading,
|
|
364
|
+
onOpenRecentActivity,
|
|
321
365
|
}: EmailRecipientFieldProps) {
|
|
322
366
|
const [value, setValue] = React.useState("")
|
|
323
367
|
const [pickerOpen, setPickerOpen] = React.useState(false)
|
|
@@ -465,6 +509,7 @@ export function EmailRecipientField({
|
|
|
465
509
|
}}
|
|
466
510
|
onSearch={onSearch}
|
|
467
511
|
searchLoading={searchLoading}
|
|
512
|
+
onOpenRecentActivity={onOpenRecentActivity}
|
|
468
513
|
/>
|
|
469
514
|
</PopoverPrimitive.Content>
|
|
470
515
|
</PopoverPrimitive.Portal>
|
|
@@ -74,6 +74,19 @@ export interface SignalPriorityPopoverProps {
|
|
|
74
74
|
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
|
|
75
75
|
/** Persisted priority-level feedback for the footer. */
|
|
76
76
|
initialPriorityFeedback?: PersistedFeedbackData | null
|
|
77
|
+
/**
|
|
78
|
+
* Optional custom trigger element. When provided, it is rendered inside
|
|
79
|
+
* `<PopoverPrimitive.Trigger asChild>` instead of the default button, so it
|
|
80
|
+
* must be a single `React.ReactElement` capable of receiving props/ref (not a
|
|
81
|
+
* generic `ReactNode`). Receives the resolved `urgencyLabel` and the current
|
|
82
|
+
* `open` state. Provide an accessible name on the element (e.g.
|
|
83
|
+
* `aria-label="View {urgencyLabel} urgency detail"`).
|
|
84
|
+
*/
|
|
85
|
+
renderTrigger?: (args: { urgencyLabel: SignalScoreUrgencyLabel; open: boolean }) => React.ReactElement
|
|
86
|
+
/** Controlled open state. When defined, the popover is controlled; otherwise it falls back to internal state. */
|
|
87
|
+
open?: boolean
|
|
88
|
+
/** Called when the open state should change. Passed straight through to the Radix root. */
|
|
89
|
+
onOpenChange?: (open: boolean) => void
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
// ---------------------------------------------------------------------------
|
|
@@ -290,11 +303,24 @@ export function SignalPriorityPopover({
|
|
|
290
303
|
initialPriorityFeedback,
|
|
291
304
|
scoreDisplay = "number",
|
|
292
305
|
formulaLabel = "Priority factors",
|
|
306
|
+
renderTrigger,
|
|
307
|
+
open: controlledOpen,
|
|
308
|
+
onOpenChange,
|
|
293
309
|
}: SignalPriorityPopoverProps) {
|
|
294
310
|
const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
|
|
295
311
|
const scoreRange = getUrgencyRange(urgencyLabel)
|
|
296
312
|
|
|
297
|
-
|
|
313
|
+
// Controlled/uncontrolled open state: use the controlled prop when defined,
|
|
314
|
+
// otherwise fall back to internal state.
|
|
315
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
|
316
|
+
const open = controlledOpen ?? uncontrolledOpen
|
|
317
|
+
const setOpen = React.useCallback(
|
|
318
|
+
(next: boolean) => {
|
|
319
|
+
if (controlledOpen === undefined) setUncontrolledOpen(next)
|
|
320
|
+
onOpenChange?.(next)
|
|
321
|
+
},
|
|
322
|
+
[controlledOpen, onOpenChange],
|
|
323
|
+
)
|
|
298
324
|
const [feedback, setFeedback] = React.useState<"positive" | "negative" | null>(null)
|
|
299
325
|
|
|
300
326
|
const triggerDefault = URGENCY_TRIGGER_DEFAULT[urgencyLabel]
|
|
@@ -306,26 +332,29 @@ export function SignalPriorityPopover({
|
|
|
306
332
|
|
|
307
333
|
return (
|
|
308
334
|
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
309
|
-
<PopoverPrimitive.Trigger asChild>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
335
|
+
<PopoverPrimitive.Trigger asChild data-testid="priority-popover-trigger">
|
|
336
|
+
{renderTrigger ? (
|
|
337
|
+
renderTrigger({ urgencyLabel, open })
|
|
338
|
+
) : (
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
className={cn(
|
|
342
|
+
"inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
343
|
+
triggerDefault,
|
|
344
|
+
triggerHover,
|
|
345
|
+
open && triggerOpen,
|
|
346
|
+
open && "outline-2 outline-foreground outline-offset-2",
|
|
347
|
+
className,
|
|
348
|
+
)}
|
|
349
|
+
>
|
|
350
|
+
{urgencyLabel} Priority
|
|
351
|
+
{open ? (
|
|
352
|
+
<ChevronUp className="h-3 w-3" />
|
|
353
|
+
) : (
|
|
354
|
+
<ChevronDown className="h-3 w-3" />
|
|
355
|
+
)}
|
|
356
|
+
</button>
|
|
357
|
+
)}
|
|
329
358
|
</PopoverPrimitive.Trigger>
|
|
330
359
|
|
|
331
360
|
<PopoverPrimitive.Portal>
|
|
@@ -84,6 +84,7 @@ export interface SuggestedContact {
|
|
|
84
84
|
lastActivity?: {
|
|
85
85
|
date: string
|
|
86
86
|
type: string
|
|
87
|
+
timelineEventId?: string
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -547,7 +548,7 @@ function EmailHeader({
|
|
|
547
548
|
onBccAdd?: (contact: SuggestedContact) => void
|
|
548
549
|
onBccRemove?: (index: number) => void
|
|
549
550
|
onOpenAccountDetails?: () => void
|
|
550
|
-
onOpenRecentActivity?: () => void
|
|
551
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void
|
|
551
552
|
iconMap?: SuggestedActionsIconMap
|
|
552
553
|
showSubject?: boolean
|
|
553
554
|
accountDetailsLabel?: string
|
|
@@ -910,7 +911,7 @@ function SuggestedActionCard({
|
|
|
910
911
|
signature?: string | React.ReactNode
|
|
911
912
|
onDuplicate?: (id: number | string) => void
|
|
912
913
|
onOpenAccountDetails?: () => void
|
|
913
|
-
onOpenRecentActivity?: () => void
|
|
914
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void
|
|
914
915
|
onMarkComplete?: (id: number | string) => void
|
|
915
916
|
onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
|
|
916
917
|
onFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
@@ -1571,7 +1572,7 @@ export interface SuggestedActionsProps {
|
|
|
1571
1572
|
signature?: string | React.ReactNode
|
|
1572
1573
|
onDuplicate?: (id: number | string) => void
|
|
1573
1574
|
onOpenAccountDetails?: () => void
|
|
1574
|
-
onOpenRecentActivity?: () => void
|
|
1575
|
+
onOpenRecentActivity?: (contact: SuggestedContact) => void
|
|
1575
1576
|
onMarkComplete?: (id: number | string) => void
|
|
1576
1577
|
onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
|
|
1577
1578
|
iconMap?: SuggestedActionsIconMap
|
|
@@ -298,7 +298,13 @@ function TimelineItem({
|
|
|
298
298
|
const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES
|
|
299
299
|
|
|
300
300
|
return (
|
|
301
|
-
<div
|
|
301
|
+
<div
|
|
302
|
+
id={`timeline-event-${event.id}`}
|
|
303
|
+
data-testid={`timeline-event-${event.id}`}
|
|
304
|
+
data-activity-id={event.id}
|
|
305
|
+
tabIndex={-1}
|
|
306
|
+
className={classes.outerRowGap}
|
|
307
|
+
>
|
|
302
308
|
{!isLast && (
|
|
303
309
|
<div className={classes.connector} />
|
|
304
310
|
)}
|