@handled-ai/design-system 0.18.31 → 0.18.33
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/draft-feedback-inline.js +10 -10
- package/dist/components/draft-feedback-inline.js.map +1 -1
- package/dist/components/score-feedback.js +5 -5
- package/dist/components/score-feedback.js.map +1 -1
- package/dist/components/suggested-actions.js +24 -5
- package/dist/components/suggested-actions.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/draft-feedback-inline.test.tsx +72 -0
- package/src/components/__tests__/suggested-actions-feedback-header.test.tsx +86 -0
- package/src/components/draft-feedback-inline.tsx +13 -12
- package/src/components/score-feedback.tsx +6 -6
- package/src/components/suggested-actions.tsx +28 -5
- package/src/index.ts +0 -1
- package/dist/components/related-record-action-card.d.ts +0 -19
- package/dist/components/related-record-action-card.js +0 -147
- package/dist/components/related-record-action-card.js.map +0 -1
- package/src/components/__tests__/related-record-action-card.test.tsx +0 -122
- package/src/components/related-record-action-card.tsx +0 -166
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DraftFeedbackInline.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Clicking the up thumb activates the positive state (monochrome bg-muted)
|
|
6
|
+
* - Clicking the down thumb activates the negative state (restrained red)
|
|
7
|
+
* - Thumb icons render outline-only (never fill="currentColor")
|
|
8
|
+
* - initialDirection="down" renders the down state pre-selected
|
|
9
|
+
* - initialDirection="up" renders the up state pre-selected
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from "vitest"
|
|
13
|
+
import React from "react"
|
|
14
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
15
|
+
import { DraftFeedbackInline } from "../draft-feedback-inline"
|
|
16
|
+
|
|
17
|
+
describe("DraftFeedbackInline", () => {
|
|
18
|
+
it("renders idle with no expanded feedback area", () => {
|
|
19
|
+
render(<DraftFeedbackInline />)
|
|
20
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
21
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
22
|
+
expect(screen.queryByText("What needs improvement?")).toBeNull()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("activates the positive state with monochrome tokens when up is clicked", () => {
|
|
26
|
+
const { container } = render(<DraftFeedbackInline />)
|
|
27
|
+
const buttons = container.querySelectorAll("button")
|
|
28
|
+
const upButton = buttons[0] as HTMLButtonElement
|
|
29
|
+
fireEvent.click(upButton)
|
|
30
|
+
|
|
31
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
32
|
+
expect(upButton.className).toContain("bg-muted")
|
|
33
|
+
expect(upButton.className).toContain("text-foreground")
|
|
34
|
+
expect(upButton.className).not.toContain("emerald")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("activates the negative state with restrained red when down is clicked", () => {
|
|
38
|
+
const { container } = render(<DraftFeedbackInline />)
|
|
39
|
+
const buttons = container.querySelectorAll("button")
|
|
40
|
+
const downButton = buttons[1] as HTMLButtonElement
|
|
41
|
+
fireEvent.click(downButton)
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
44
|
+
expect(downButton.className).toContain("bg-red-50")
|
|
45
|
+
expect(downButton.className).toContain("text-red-600")
|
|
46
|
+
expect(downButton.className).not.toContain("bg-red-100")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("renders thumb icons outline-only (never fill=currentColor)", () => {
|
|
50
|
+
const { container } = render(<DraftFeedbackInline />)
|
|
51
|
+
const buttons = container.querySelectorAll("button")
|
|
52
|
+
// Activate up so the icon would be "filled" under the old behavior.
|
|
53
|
+
fireEvent.click(buttons[0] as HTMLButtonElement)
|
|
54
|
+
|
|
55
|
+
const svgs = container.querySelectorAll("svg")
|
|
56
|
+
svgs.forEach((svg) => {
|
|
57
|
+
expect(svg.getAttribute("fill")).not.toBe("currentColor")
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("renders the down state pre-selected with initialDirection='down'", () => {
|
|
62
|
+
render(<DraftFeedbackInline initialDirection="down" />)
|
|
63
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
64
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("renders the up state pre-selected with initialDirection='up'", () => {
|
|
68
|
+
render(<DraftFeedbackInline initialDirection="up" />)
|
|
69
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
70
|
+
expect(screen.queryByText("What needs improvement?")).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the SuggestedActions card header thumbs (feedback direction).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Clicking up opens the feedback panel with direction "up"
|
|
6
|
+
* - Clicking down opens the feedback panel with direction "down"
|
|
7
|
+
* - Clicking the same direction again closes the panel
|
|
8
|
+
* - Switching direction remounts DraftFeedbackInline (panel stays open, content switches)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest"
|
|
12
|
+
import React from "react"
|
|
13
|
+
import { render, screen, fireEvent, within } from "@testing-library/react"
|
|
14
|
+
import { SuggestedActions } from "../suggested-actions"
|
|
15
|
+
import type { SuggestedAction } from "../suggested-actions"
|
|
16
|
+
|
|
17
|
+
const action: SuggestedAction = {
|
|
18
|
+
id: 1,
|
|
19
|
+
type: "email",
|
|
20
|
+
label: "Send follow-up email",
|
|
21
|
+
status: "pending",
|
|
22
|
+
content: "<p>Hello there.</p>",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The header thumbs are the only buttons that wrap a thumbs-up/down SVG and
|
|
26
|
+
// live in the card header. We resolve them by locating the lucide icon class.
|
|
27
|
+
function getHeaderThumbs(container: HTMLElement) {
|
|
28
|
+
const up = container.querySelector("button .lucide-thumbs-up")?.closest("button")
|
|
29
|
+
const down = container.querySelector("button .lucide-thumbs-down")?.closest("button")
|
|
30
|
+
return { up: up as HTMLButtonElement, down: down as HTMLButtonElement }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("SuggestedActions header thumbs", () => {
|
|
34
|
+
it("does not show the feedback panel initially", () => {
|
|
35
|
+
render(<SuggestedActions actions={[action]} />)
|
|
36
|
+
expect(screen.queryByText("How's this draft?")).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("opens the feedback panel with direction up when up is clicked", () => {
|
|
40
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
41
|
+
const { up } = getHeaderThumbs(container)
|
|
42
|
+
fireEvent.click(up)
|
|
43
|
+
|
|
44
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
45
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
46
|
+
expect(screen.queryByText("What needs improvement?")).toBeNull()
|
|
47
|
+
expect(up.className).toContain("bg-muted")
|
|
48
|
+
expect(up.className).toContain("text-foreground")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("opens the feedback panel with direction down when down is clicked", () => {
|
|
52
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
53
|
+
const { down } = getHeaderThumbs(container)
|
|
54
|
+
fireEvent.click(down)
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
57
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
58
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
59
|
+
expect(down.className).toContain("bg-red-50")
|
|
60
|
+
expect(down.className).toContain("text-red-600")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("closes the panel when the same direction is clicked again", () => {
|
|
64
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
65
|
+
const { up } = getHeaderThumbs(container)
|
|
66
|
+
fireEvent.click(up)
|
|
67
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
68
|
+
|
|
69
|
+
fireEvent.click(up)
|
|
70
|
+
expect(screen.queryByText("How's this draft?")).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("switches direction (remounts DraftFeedbackInline) while keeping the panel open", () => {
|
|
74
|
+
const { container } = render(<SuggestedActions actions={[action]} />)
|
|
75
|
+
const { up, down } = getHeaderThumbs(container)
|
|
76
|
+
|
|
77
|
+
fireEvent.click(up)
|
|
78
|
+
expect(screen.getByText("What worked well?")).toBeTruthy()
|
|
79
|
+
|
|
80
|
+
fireEvent.click(down)
|
|
81
|
+
// Panel still open, but now showing the negative (down) variant.
|
|
82
|
+
expect(screen.getByText("How's this draft?")).toBeTruthy()
|
|
83
|
+
expect(screen.getByText("What needs improvement?")).toBeTruthy()
|
|
84
|
+
expect(screen.queryByText("What worked well?")).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -70,7 +70,7 @@ export function DraftFeedbackInline({
|
|
|
70
70
|
if (noted) {
|
|
71
71
|
return (
|
|
72
72
|
<div className="flex items-center gap-1.5 py-1 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
73
|
-
<Check className="w-3.5 h-3.5 text-
|
|
73
|
+
<Check className="w-3.5 h-3.5 text-foreground" />
|
|
74
74
|
<span className="text-xs text-muted-foreground">Feedback recorded</span>
|
|
75
75
|
</div>
|
|
76
76
|
)
|
|
@@ -88,7 +88,7 @@ export function DraftFeedbackInline({
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
|
-
<div className="space-y-
|
|
91
|
+
<div className="space-y-2">
|
|
92
92
|
<div className="flex items-center justify-between">
|
|
93
93
|
<span className="text-sm text-foreground font-medium">How's this draft?</span>
|
|
94
94
|
<div className="flex gap-1">
|
|
@@ -100,11 +100,11 @@ export function DraftFeedbackInline({
|
|
|
100
100
|
}}
|
|
101
101
|
className={`p-1.5 rounded transition-colors ${
|
|
102
102
|
thumbState === "up"
|
|
103
|
-
? "bg-
|
|
103
|
+
? "bg-muted text-foreground"
|
|
104
104
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
105
105
|
}`}
|
|
106
106
|
>
|
|
107
|
-
<ThumbsUp className="w-4 h-4"
|
|
107
|
+
<ThumbsUp className="w-4 h-4" />
|
|
108
108
|
</button>
|
|
109
109
|
<button
|
|
110
110
|
onClick={() => {
|
|
@@ -114,31 +114,31 @@ export function DraftFeedbackInline({
|
|
|
114
114
|
}}
|
|
115
115
|
className={`p-1.5 rounded transition-colors ${
|
|
116
116
|
thumbState === "down"
|
|
117
|
-
? "bg-red-
|
|
117
|
+
? "bg-red-50 text-red-600 dark:bg-red-950/30 dark:text-red-400"
|
|
118
118
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
119
119
|
}`}
|
|
120
120
|
>
|
|
121
|
-
<ThumbsDown className="w-4 h-4"
|
|
121
|
+
<ThumbsDown className="w-4 h-4" />
|
|
122
122
|
</button>
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
126
|
{thumbState && (
|
|
127
|
-
<div className="pt-
|
|
127
|
+
<div className="pt-2 space-y-2.5 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
128
128
|
<div>
|
|
129
129
|
<span className="text-xs text-muted-foreground mb-2 block font-medium">
|
|
130
130
|
{thumbState === "up" ? "What worked well?" : "What needs improvement?"}
|
|
131
131
|
</span>
|
|
132
|
-
<div className="flex flex-wrap gap-
|
|
132
|
+
<div className="flex flex-wrap gap-2">
|
|
133
133
|
{(thumbState === "up" ? positivePills : negativePills).map((pill) => (
|
|
134
134
|
<button
|
|
135
135
|
key={pill}
|
|
136
136
|
onClick={() => togglePill(pill)}
|
|
137
|
-
className={`px-
|
|
137
|
+
className={`px-3 py-1.5 rounded-full text-[11px] font-medium border transition-colors ${
|
|
138
138
|
selectedPills.includes(pill)
|
|
139
139
|
? thumbState === "up"
|
|
140
|
-
? "bg-
|
|
141
|
-
: "bg-red-
|
|
140
|
+
? "bg-muted text-foreground border-border"
|
|
141
|
+
: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-800"
|
|
142
142
|
: "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
|
|
143
143
|
}`}
|
|
144
144
|
>
|
|
@@ -155,7 +155,7 @@ export function DraftFeedbackInline({
|
|
|
155
155
|
className="w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:border-indigo-500/50 resize-none min-h-[60px]"
|
|
156
156
|
/>
|
|
157
157
|
|
|
158
|
-
<div className="flex items-center gap-2 pt-
|
|
158
|
+
<div className="flex items-center gap-2.5 pt-2">
|
|
159
159
|
{thumbState === "down" ? (
|
|
160
160
|
<>
|
|
161
161
|
<button
|
|
@@ -191,3 +191,4 @@ export function DraftFeedbackInline({
|
|
|
191
191
|
</div>
|
|
192
192
|
)
|
|
193
193
|
}
|
|
194
|
+
|
|
@@ -163,11 +163,11 @@ function Trigger({ className }: { className?: string }) {
|
|
|
163
163
|
className={cn(
|
|
164
164
|
"p-1.5 rounded transition-colors",
|
|
165
165
|
thumbState === "up"
|
|
166
|
-
? "bg-
|
|
166
|
+
? "bg-muted text-foreground"
|
|
167
167
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
168
168
|
)}
|
|
169
169
|
>
|
|
170
|
-
<ThumbsUp className="w-3.5 h-3.5"
|
|
170
|
+
<ThumbsUp className="w-3.5 h-3.5" />
|
|
171
171
|
</button>
|
|
172
172
|
<button
|
|
173
173
|
type="button"
|
|
@@ -175,11 +175,11 @@ function Trigger({ className }: { className?: string }) {
|
|
|
175
175
|
className={cn(
|
|
176
176
|
"p-1.5 rounded transition-colors",
|
|
177
177
|
thumbState === "down"
|
|
178
|
-
? "bg-red-
|
|
178
|
+
? "bg-red-50 text-red-600 dark:bg-red-950/30 dark:text-red-400"
|
|
179
179
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
180
180
|
)}
|
|
181
181
|
>
|
|
182
|
-
<ThumbsDown className="w-3.5 h-3.5"
|
|
182
|
+
<ThumbsDown className="w-3.5 h-3.5" />
|
|
183
183
|
</button>
|
|
184
184
|
</div>
|
|
185
185
|
)
|
|
@@ -219,8 +219,8 @@ function Panel({ className }: { className?: string }) {
|
|
|
219
219
|
"px-2.5 py-1 rounded-full text-[11px] font-medium border transition-colors",
|
|
220
220
|
selectedPills.includes(pill)
|
|
221
221
|
? thumbState === "up"
|
|
222
|
-
? "bg-
|
|
223
|
-
: "bg-red-
|
|
222
|
+
? "bg-muted text-foreground border-border"
|
|
223
|
+
: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-800"
|
|
224
224
|
: "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
|
|
225
225
|
)}
|
|
226
226
|
>
|
|
@@ -925,6 +925,7 @@ function SuggestedActionCard({
|
|
|
925
925
|
)
|
|
926
926
|
const [showAiEdit, setShowAiEdit] = React.useState(false)
|
|
927
927
|
const [feedbackOpen, setFeedbackOpen] = React.useState(false)
|
|
928
|
+
const [feedbackDirection, setFeedbackDirection] = React.useState<"up" | "down" | null>(null)
|
|
928
929
|
const [followUpEnabled, setFollowUpEnabled] = React.useState(action.followUp?.enabled ?? false)
|
|
929
930
|
const [threadExpanded, setThreadExpanded] = React.useState(false)
|
|
930
931
|
const [expandedMessageId, setExpandedMessageId] = React.useState<string | null>(null)
|
|
@@ -1016,18 +1017,38 @@ function SuggestedActionCard({
|
|
|
1016
1017
|
</div>
|
|
1017
1018
|
<div className="flex items-center gap-1.5">
|
|
1018
1019
|
<button
|
|
1019
|
-
onClick={() =>
|
|
1020
|
+
onClick={() => {
|
|
1021
|
+
if (feedbackOpen && feedbackDirection === "up") {
|
|
1022
|
+
setFeedbackOpen(false)
|
|
1023
|
+
setFeedbackDirection(null)
|
|
1024
|
+
} else {
|
|
1025
|
+
setFeedbackDirection("up")
|
|
1026
|
+
setFeedbackOpen(true)
|
|
1027
|
+
}
|
|
1028
|
+
}}
|
|
1020
1029
|
className={`p-1.5 rounded transition-colors ${
|
|
1021
|
-
feedbackOpen
|
|
1022
|
-
? "bg-
|
|
1030
|
+
feedbackOpen && feedbackDirection === "up"
|
|
1031
|
+
? "bg-muted text-foreground"
|
|
1023
1032
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
1024
1033
|
}`}
|
|
1025
1034
|
>
|
|
1026
1035
|
<ThumbsUp className="w-3.5 h-3.5" />
|
|
1027
1036
|
</button>
|
|
1028
1037
|
<button
|
|
1029
|
-
onClick={() =>
|
|
1030
|
-
|
|
1038
|
+
onClick={() => {
|
|
1039
|
+
if (feedbackOpen && feedbackDirection === "down") {
|
|
1040
|
+
setFeedbackOpen(false)
|
|
1041
|
+
setFeedbackDirection(null)
|
|
1042
|
+
} else {
|
|
1043
|
+
setFeedbackDirection("down")
|
|
1044
|
+
setFeedbackOpen(true)
|
|
1045
|
+
}
|
|
1046
|
+
}}
|
|
1047
|
+
className={`p-1.5 rounded transition-colors ${
|
|
1048
|
+
feedbackOpen && feedbackDirection === "down"
|
|
1049
|
+
? "bg-red-50 text-red-600 dark:bg-red-950/30 dark:text-red-400"
|
|
1050
|
+
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
1051
|
+
}`}
|
|
1031
1052
|
>
|
|
1032
1053
|
<ThumbsDown className="w-3.5 h-3.5" />
|
|
1033
1054
|
</button>
|
|
@@ -1047,6 +1068,8 @@ function SuggestedActionCard({
|
|
|
1047
1068
|
{feedbackOpen && (
|
|
1048
1069
|
<div className="px-5 py-3 border-b border-border/40 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
1049
1070
|
<DraftFeedbackInline
|
|
1071
|
+
key={`feedback-${feedbackDirection}`}
|
|
1072
|
+
initialDirection={feedbackDirection}
|
|
1050
1073
|
onRegenerateRequest={(pills, detail) => {
|
|
1051
1074
|
onFeedback?.("down", pills, detail)
|
|
1052
1075
|
}}
|
package/src/index.ts
CHANGED
|
@@ -76,7 +76,6 @@ export {
|
|
|
76
76
|
} from "./components/quick-action-modal"
|
|
77
77
|
export * from "./components/quick-action-sidebar-nav"
|
|
78
78
|
export * from "./components/recommended-actions-section"
|
|
79
|
-
export * from "./components/related-record-action-card"
|
|
80
79
|
export * from "./components/report-card"
|
|
81
80
|
export * from "./components/rich-text-toolbar"
|
|
82
81
|
export * from "./components/score-analysis-modal"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
type RelatedRecordActionCardKind = "case" | "account" | "opportunity" | "salesforce" | "generic";
|
|
4
|
-
type RelatedRecordActionIcon = "salesforce" | React.ReactNode;
|
|
5
|
-
interface RelatedRecordActionCardProps {
|
|
6
|
-
kind: RelatedRecordActionCardKind;
|
|
7
|
-
label: string;
|
|
8
|
-
subtitle?: string;
|
|
9
|
-
disabledReason?: string;
|
|
10
|
-
href?: string;
|
|
11
|
-
external?: boolean;
|
|
12
|
-
icon?: RelatedRecordActionIcon;
|
|
13
|
-
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
14
|
-
className?: string;
|
|
15
|
-
testId?: string;
|
|
16
|
-
}
|
|
17
|
-
declare function RelatedRecordActionCard({ kind, label, subtitle, disabledReason, href, external, icon, onClick, className, testId, }: RelatedRecordActionCardProps): React.JSX.Element;
|
|
18
|
-
|
|
19
|
-
export { RelatedRecordActionCard, type RelatedRecordActionCardKind, type RelatedRecordActionCardProps, type RelatedRecordActionIcon };
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
"use client";
|
|
4
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { ExternalLink } from "lucide-react";
|
|
6
|
-
import { BRAND_ICONS } from "../lib/icons.js";
|
|
7
|
-
import { cn } from "../lib/utils.js";
|
|
8
|
-
function renderActionIcon(icon, kind) {
|
|
9
|
-
if (icon === "salesforce" || kind === "salesforce") {
|
|
10
|
-
return /* @__PURE__ */ jsx(
|
|
11
|
-
"img",
|
|
12
|
-
{
|
|
13
|
-
src: BRAND_ICONS.salesforce,
|
|
14
|
-
alt: "Salesforce",
|
|
15
|
-
className: "h-4 w-4 object-contain",
|
|
16
|
-
draggable: false
|
|
17
|
-
}
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
if (icon) {
|
|
21
|
-
return icon;
|
|
22
|
-
}
|
|
23
|
-
return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: kind.slice(0, 1).toUpperCase() });
|
|
24
|
-
}
|
|
25
|
-
function RelatedRecordActionCard({
|
|
26
|
-
kind,
|
|
27
|
-
label,
|
|
28
|
-
subtitle,
|
|
29
|
-
disabledReason,
|
|
30
|
-
href,
|
|
31
|
-
external,
|
|
32
|
-
icon,
|
|
33
|
-
onClick,
|
|
34
|
-
className,
|
|
35
|
-
testId
|
|
36
|
-
}) {
|
|
37
|
-
const isDisabled = Boolean(disabledReason) || !href && !onClick;
|
|
38
|
-
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
39
|
-
/* @__PURE__ */ jsx(
|
|
40
|
-
"span",
|
|
41
|
-
{
|
|
42
|
-
"data-slot": "related-record-action-card-icon",
|
|
43
|
-
className: cn(
|
|
44
|
-
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border text-xs font-semibold transition-colors",
|
|
45
|
-
isDisabled ? "border-border/60 bg-muted/40 text-muted-foreground" : "border-border bg-muted/30 text-muted-foreground group-hover:bg-muted/60 group-active:text-primary"
|
|
46
|
-
),
|
|
47
|
-
children: renderActionIcon(icon, kind)
|
|
48
|
-
}
|
|
49
|
-
),
|
|
50
|
-
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
51
|
-
/* @__PURE__ */ jsx(
|
|
52
|
-
"span",
|
|
53
|
-
{
|
|
54
|
-
"data-slot": "related-record-action-card-label",
|
|
55
|
-
className: cn(
|
|
56
|
-
"block truncate text-sm font-medium transition-colors",
|
|
57
|
-
isDisabled ? "text-muted-foreground" : "text-foreground group-active:text-primary"
|
|
58
|
-
),
|
|
59
|
-
children: label
|
|
60
|
-
}
|
|
61
|
-
),
|
|
62
|
-
subtitle && /* @__PURE__ */ jsx(
|
|
63
|
-
"span",
|
|
64
|
-
{
|
|
65
|
-
"data-slot": "related-record-action-card-subtitle",
|
|
66
|
-
className: "mt-0.5 block truncate text-xs text-muted-foreground",
|
|
67
|
-
children: subtitle
|
|
68
|
-
}
|
|
69
|
-
),
|
|
70
|
-
disabledReason && /* @__PURE__ */ jsx(
|
|
71
|
-
"span",
|
|
72
|
-
{
|
|
73
|
-
"data-slot": "related-record-action-card-disabled-reason",
|
|
74
|
-
className: "mt-1 block text-xs text-muted-foreground",
|
|
75
|
-
children: disabledReason
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
|
-
] }),
|
|
79
|
-
external && /* @__PURE__ */ jsx(
|
|
80
|
-
ExternalLink,
|
|
81
|
-
{
|
|
82
|
-
"aria-hidden": "true",
|
|
83
|
-
"data-slot": "related-record-action-card-external-icon",
|
|
84
|
-
className: cn(
|
|
85
|
-
"h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors",
|
|
86
|
-
isDisabled ? "" : "group-active:text-primary"
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
)
|
|
90
|
-
] });
|
|
91
|
-
if (isDisabled) {
|
|
92
|
-
return /* @__PURE__ */ jsx(
|
|
93
|
-
"div",
|
|
94
|
-
{
|
|
95
|
-
"aria-disabled": "true",
|
|
96
|
-
"data-kind": kind,
|
|
97
|
-
"data-slot": "related-record-action-card",
|
|
98
|
-
"data-testid": testId,
|
|
99
|
-
className: cn(
|
|
100
|
-
"group flex w-full items-center gap-3 rounded-lg border px-3 py-2 text-left transition-colors",
|
|
101
|
-
"cursor-not-allowed border-border/60 bg-muted/20 text-muted-foreground opacity-70",
|
|
102
|
-
className
|
|
103
|
-
),
|
|
104
|
-
children: content
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
if (href) {
|
|
109
|
-
return /* @__PURE__ */ jsx(
|
|
110
|
-
"a",
|
|
111
|
-
{
|
|
112
|
-
href,
|
|
113
|
-
target: external ? "_blank" : void 0,
|
|
114
|
-
rel: external ? "noopener noreferrer" : void 0,
|
|
115
|
-
"data-kind": kind,
|
|
116
|
-
"data-slot": "related-record-action-card",
|
|
117
|
-
"data-testid": testId,
|
|
118
|
-
className: cn(
|
|
119
|
-
"group flex w-full items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 text-left transition-colors",
|
|
120
|
-
"cursor-pointer hover:bg-muted/50 hover:border-border/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:text-primary",
|
|
121
|
-
className
|
|
122
|
-
),
|
|
123
|
-
children: content
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
return /* @__PURE__ */ jsx(
|
|
128
|
-
"button",
|
|
129
|
-
{
|
|
130
|
-
type: "button",
|
|
131
|
-
onClick,
|
|
132
|
-
"data-kind": kind,
|
|
133
|
-
"data-slot": "related-record-action-card",
|
|
134
|
-
"data-testid": testId,
|
|
135
|
-
className: cn(
|
|
136
|
-
"group flex w-full items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 text-left transition-colors",
|
|
137
|
-
"cursor-pointer hover:bg-muted/50 hover:border-border/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:text-primary",
|
|
138
|
-
className
|
|
139
|
-
),
|
|
140
|
-
children: content
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
export {
|
|
145
|
-
RelatedRecordActionCard
|
|
146
|
-
};
|
|
147
|
-
//# sourceMappingURL=related-record-action-card.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/related-record-action-card.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ExternalLink } from \"lucide-react\"\n\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { cn } from \"../lib/utils\"\n\nexport type RelatedRecordActionCardKind = \"case\" | \"account\" | \"opportunity\" | \"salesforce\" | \"generic\"\n\nexport type RelatedRecordActionIcon = \"salesforce\" | React.ReactNode\n\nexport interface RelatedRecordActionCardProps {\n kind: RelatedRecordActionCardKind\n label: string\n subtitle?: string\n disabledReason?: string\n href?: string\n external?: boolean\n icon?: RelatedRecordActionIcon\n onClick?: React.MouseEventHandler<HTMLButtonElement>\n className?: string\n testId?: string\n}\n\nfunction renderActionIcon(icon: RelatedRecordActionIcon | undefined, kind: RelatedRecordActionCardKind) {\n if (icon === \"salesforce\" || kind === \"salesforce\") {\n return (\n <img\n src={BRAND_ICONS.salesforce}\n alt=\"Salesforce\"\n className=\"h-4 w-4 object-contain\"\n draggable={false}\n />\n )\n }\n\n if (icon) {\n return icon\n }\n\n return <span aria-hidden=\"true\">{kind.slice(0, 1).toUpperCase()}</span>\n}\n\nexport function RelatedRecordActionCard({\n kind,\n label,\n subtitle,\n disabledReason,\n href,\n external,\n icon,\n onClick,\n className,\n testId,\n}: RelatedRecordActionCardProps) {\n const isDisabled = Boolean(disabledReason) || (!href && !onClick)\n\n const content = (\n <>\n <span\n data-slot=\"related-record-action-card-icon\"\n className={cn(\n \"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border text-xs font-semibold transition-colors\",\n isDisabled\n ? \"border-border/60 bg-muted/40 text-muted-foreground\"\n : \"border-border bg-muted/30 text-muted-foreground group-hover:bg-muted/60 group-active:text-primary\"\n )}\n >\n {renderActionIcon(icon, kind)}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span\n data-slot=\"related-record-action-card-label\"\n className={cn(\n \"block truncate text-sm font-medium transition-colors\",\n isDisabled ? \"text-muted-foreground\" : \"text-foreground group-active:text-primary\"\n )}\n >\n {label}\n </span>\n {subtitle && (\n <span\n data-slot=\"related-record-action-card-subtitle\"\n className=\"mt-0.5 block truncate text-xs text-muted-foreground\"\n >\n {subtitle}\n </span>\n )}\n {disabledReason && (\n <span\n data-slot=\"related-record-action-card-disabled-reason\"\n className=\"mt-1 block text-xs text-muted-foreground\"\n >\n {disabledReason}\n </span>\n )}\n </span>\n {external && (\n <ExternalLink\n aria-hidden=\"true\"\n data-slot=\"related-record-action-card-external-icon\"\n className={cn(\n \"h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors\",\n isDisabled ? \"\" : \"group-active:text-primary\"\n )}\n />\n )}\n </>\n )\n\n if (isDisabled) {\n return (\n <div\n aria-disabled=\"true\"\n data-kind={kind}\n data-slot=\"related-record-action-card\"\n data-testid={testId}\n className={cn(\n \"group flex w-full items-center gap-3 rounded-lg border px-3 py-2 text-left transition-colors\",\n \"cursor-not-allowed border-border/60 bg-muted/20 text-muted-foreground opacity-70\",\n className\n )}\n >\n {content}\n </div>\n )\n }\n\n if (href) {\n return (\n <a\n href={href}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n data-kind={kind}\n data-slot=\"related-record-action-card\"\n data-testid={testId}\n className={cn(\n \"group flex w-full items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 text-left transition-colors\",\n \"cursor-pointer hover:bg-muted/50 hover:border-border/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:text-primary\",\n className\n )}\n >\n {content}\n </a>\n )\n }\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n data-kind={kind}\n data-slot=\"related-record-action-card\"\n data-testid={testId}\n className={cn(\n \"group flex w-full items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 text-left transition-colors\",\n \"cursor-pointer hover:bg-muted/50 hover:border-border/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:text-primary\",\n className\n )}\n >\n {content}\n </button>\n )\n}\n"],"mappings":";AA4BM,SA+BF,UA/BE,KA2CA,YA3CA;AAzBN,SAAS,oBAAoB;AAE7B,SAAS,mBAAmB;AAC5B,SAAS,UAAU;AAmBnB,SAAS,iBAAiB,MAA2C,MAAmC;AACtG,MAAI,SAAS,gBAAgB,SAAS,cAAc;AAClD,WACE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,YAAY;AAAA,QACjB,KAAI;AAAA,QACJ,WAAU;AAAA,QACV,WAAW;AAAA;AAAA,IACb;AAAA,EAEJ;AAEA,MAAI,MAAM;AACR,WAAO;AAAA,EACT;AAEA,SAAO,oBAAC,UAAK,eAAY,QAAQ,eAAK,MAAM,GAAG,CAAC,EAAE,YAAY,GAAE;AAClE;AAEO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,aAAa,QAAQ,cAAc,KAAM,CAAC,QAAQ,CAAC;AAEzD,QAAM,UACJ,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA,aACI,uDACA;AAAA,QACN;AAAA,QAEC,2BAAiB,MAAM,IAAI;AAAA;AAAA,IAC9B;AAAA,IACA,qBAAC,UAAK,WAAU,kBACd;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA,aAAa,0BAA0B;AAAA,UACzC;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MACC,YACC;AAAA,QAAC;AAAA;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MAED,kBACC;AAAA,QAAC;AAAA;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,OAEJ;AAAA,IACC,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA,aAAa,KAAK;AAAA,QACpB;AAAA;AAAA,IACF;AAAA,KAEJ;AAGF,MAAI,YAAY;AACd,WACE;AAAA,MAAC;AAAA;AAAA,QACC,iBAAc;AAAA,QACd,aAAW;AAAA,QACX,aAAU;AAAA,QACV,eAAa;AAAA,QACb,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,MAAI,MAAM;AACR,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,WAAW,WAAW;AAAA,QAC9B,KAAK,WAAW,wBAAwB;AAAA,QACxC,aAAW;AAAA,QACX,aAAU;AAAA,QACV,eAAa;AAAA,QACb,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,aAAW;AAAA,MACX,aAAU;AAAA,MACV,eAAa;AAAA,MACb,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import "@testing-library/jest-dom/vitest"
|
|
2
|
-
|
|
3
|
-
import React from "react"
|
|
4
|
-
import { fireEvent, render, screen } from "@testing-library/react"
|
|
5
|
-
import { describe, expect, it, vi } from "vitest"
|
|
6
|
-
|
|
7
|
-
import { BRAND_ICONS } from "../../lib/icons"
|
|
8
|
-
import { RelatedRecordActionCard } from "../related-record-action-card"
|
|
9
|
-
|
|
10
|
-
describe("RelatedRecordActionCard", () => {
|
|
11
|
-
it("renders an enabled href as a link with pointer affordance", () => {
|
|
12
|
-
render(
|
|
13
|
-
<RelatedRecordActionCard
|
|
14
|
-
kind="account"
|
|
15
|
-
label="Open account"
|
|
16
|
-
subtitle="Acme Corp"
|
|
17
|
-
href="/accounts/acme"
|
|
18
|
-
testId="record-card"
|
|
19
|
-
/>
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
const card = screen.getByTestId("record-card")
|
|
23
|
-
|
|
24
|
-
expect(card.tagName).toBe("A")
|
|
25
|
-
expect(card).toHaveAttribute("href", "/accounts/acme")
|
|
26
|
-
expect(card.className).toContain("cursor-pointer")
|
|
27
|
-
expect(card.className).toContain("hover:bg-muted/50")
|
|
28
|
-
expect(card.className).toContain("hover:border-border/80")
|
|
29
|
-
expect(card.className).toContain("focus-visible:ring-2")
|
|
30
|
-
expect(card.className).toContain("active:text-primary")
|
|
31
|
-
expect(screen.getByText("Acme Corp")).toBeInTheDocument()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it("renders an enabled onClick action as a button and invokes the callback", () => {
|
|
35
|
-
const onClick = vi.fn()
|
|
36
|
-
|
|
37
|
-
render(
|
|
38
|
-
<RelatedRecordActionCard
|
|
39
|
-
kind="case"
|
|
40
|
-
label="Open case"
|
|
41
|
-
onClick={onClick}
|
|
42
|
-
testId="record-card"
|
|
43
|
-
/>
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
const card = screen.getByRole("button", { name: /open case/i })
|
|
47
|
-
|
|
48
|
-
expect(card.tagName).toBe("BUTTON")
|
|
49
|
-
expect(card).toHaveAttribute("type", "button")
|
|
50
|
-
expect(card.className).toContain("cursor-pointer")
|
|
51
|
-
|
|
52
|
-
fireEvent.click(card)
|
|
53
|
-
|
|
54
|
-
expect(onClick).toHaveBeenCalledTimes(1)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it("renders a disabled card when disabledReason is present", () => {
|
|
58
|
-
render(
|
|
59
|
-
<RelatedRecordActionCard
|
|
60
|
-
kind="opportunity"
|
|
61
|
-
label="Open opportunity"
|
|
62
|
-
disabledReason="Connect Salesforce to view this opportunity."
|
|
63
|
-
href="/opportunities/1"
|
|
64
|
-
testId="record-card"
|
|
65
|
-
/>
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
const card = screen.getByTestId("record-card")
|
|
69
|
-
|
|
70
|
-
expect(card.tagName).toBe("DIV")
|
|
71
|
-
expect(card).toHaveAttribute("aria-disabled", "true")
|
|
72
|
-
expect(card.className).toContain("cursor-not-allowed")
|
|
73
|
-
expect(card.className).toContain("text-muted-foreground")
|
|
74
|
-
expect(card.className).not.toContain("cursor-pointer")
|
|
75
|
-
expect(card.className).not.toContain("hover:bg-muted/50")
|
|
76
|
-
expect(screen.getByText("Connect Salesforce to view this opportunity.")).toBeInTheDocument()
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it("renders a disabled card when no action is available", () => {
|
|
80
|
-
render(<RelatedRecordActionCard kind="generic" label="Unavailable record" testId="record-card" />)
|
|
81
|
-
|
|
82
|
-
const card = screen.getByTestId("record-card")
|
|
83
|
-
|
|
84
|
-
expect(card.tagName).toBe("DIV")
|
|
85
|
-
expect(card).toHaveAttribute("aria-disabled", "true")
|
|
86
|
-
expect(card.className).toContain("cursor-not-allowed")
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it("sets external link attributes and shows the external icon", () => {
|
|
90
|
-
const { container } = render(
|
|
91
|
-
<RelatedRecordActionCard
|
|
92
|
-
kind="salesforce"
|
|
93
|
-
label="Open in Salesforce"
|
|
94
|
-
href="https://example.salesforce.com/001"
|
|
95
|
-
external
|
|
96
|
-
testId="record-card"
|
|
97
|
-
/>
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
const card = screen.getByTestId("record-card")
|
|
101
|
-
|
|
102
|
-
expect(card).toHaveAttribute("target", "_blank")
|
|
103
|
-
expect(card).toHaveAttribute("rel", "noopener noreferrer")
|
|
104
|
-
expect(container.querySelector('[data-slot="related-record-action-card-external-icon"]')).not.toBeNull()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it("renders the Salesforce brand icon when icon is salesforce", () => {
|
|
108
|
-
const { container } = render(
|
|
109
|
-
<RelatedRecordActionCard
|
|
110
|
-
kind="generic"
|
|
111
|
-
label="Salesforce record"
|
|
112
|
-
href="https://example.salesforce.com/001"
|
|
113
|
-
icon="salesforce"
|
|
114
|
-
/>
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
const icon = container.querySelector('[data-slot="related-record-action-card-icon"] img')
|
|
118
|
-
|
|
119
|
-
expect(icon).toHaveAttribute("src", BRAND_ICONS.salesforce)
|
|
120
|
-
expect(icon).toHaveAttribute("alt", "Salesforce")
|
|
121
|
-
})
|
|
122
|
-
})
|