@handled-ai/design-system 0.18.49 → 0.18.51
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/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-feedback-inline.d.ts +12 -28
- package/dist/components/signal-feedback-inline.js +10 -146
- package/dist/components/signal-feedback-inline.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +16 -7
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +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 +3 -3
- package/dist/prototype/prototype-inbox-view.js +3 -1
- package/dist/prototype/prototype-inbox-view.js.map +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-QJngMAj7.d.ts → signal-priority-popover-DTedstRL.d.ts} +13 -4
- package/package.json +1 -1
- package/src/components/__tests__/case-panel-why.test.tsx +0 -126
- package/src/components/__tests__/signal-priority-popover.test.tsx +41 -4
- package/src/components/signal-feedback-inline.tsx +20 -181
- package/src/components/signal-priority-popover.tsx +19 -6
- package/src/index.ts +1 -1
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +34 -0
- package/src/prototype/prototype-config.ts +7 -3
- package/src/prototype/prototype-inbox-view.tsx +5 -3
- package/src/prototype/__tests__/detail-view-opportunity-preview.test.tsx +0 -90
|
@@ -119,8 +119,8 @@ describe("SignalPriorityPopover", () => {
|
|
|
119
119
|
|
|
120
120
|
// Check head section
|
|
121
121
|
expect(content.textContent).toContain("Why this is high priority")
|
|
122
|
-
expect(
|
|
123
|
-
expect(
|
|
122
|
+
expect(screen.getByTestId("priority-overall-score").textContent).toContain("79")
|
|
123
|
+
expect(screen.getByTestId("priority-overall-score").textContent).toContain("/100")
|
|
124
124
|
expect(content.textContent).toContain("High range")
|
|
125
125
|
expect(content.textContent).toContain("60-79")
|
|
126
126
|
})
|
|
@@ -188,13 +188,50 @@ describe("SignalPriorityPopover", () => {
|
|
|
188
188
|
expect(row.textContent).not.toContain("Raises0/100")
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it("renders Contributing factors section label", () => {
|
|
191
|
+
it("renders Contributing factors section label and shared-consumer-safe default formula label", () => {
|
|
192
192
|
render(<SignalPriorityPopover {...defaultProps} />)
|
|
193
193
|
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
194
194
|
|
|
195
195
|
const content = screen.getByTestId("priority-popover-content")
|
|
196
196
|
expect(content.textContent).toContain("Contributing factors")
|
|
197
|
-
expect(content.textContent).toContain("
|
|
197
|
+
expect(content.textContent).toContain("Priority factors")
|
|
198
|
+
expect(content.textContent).not.toContain("Score = weighted sum")
|
|
199
|
+
expect(content.textContent).not.toContain("Priority = weighted signals + calibration")
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("renders a custom formula label when provided", () => {
|
|
203
|
+
render(
|
|
204
|
+
<SignalPriorityPopover
|
|
205
|
+
{...defaultProps}
|
|
206
|
+
formulaLabel="Priority = weighted signals + calibration"
|
|
207
|
+
/>,
|
|
208
|
+
)
|
|
209
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
210
|
+
|
|
211
|
+
const content = screen.getByTestId("priority-popover-content")
|
|
212
|
+
expect(content.textContent).toContain("Priority = weighted signals + calibration")
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
it("renders the overall score number by default while preserving factor row scores", () => {
|
|
217
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
218
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
219
|
+
|
|
220
|
+
expect(screen.getByTestId("priority-overall-score").textContent).toBe("79/100")
|
|
221
|
+
expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
|
|
222
|
+
expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it("hides only the overall header score in label display mode", () => {
|
|
226
|
+
render(<SignalPriorityPopover {...defaultProps} scoreDisplay="label" />)
|
|
227
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
228
|
+
|
|
229
|
+
const header = screen.getByTestId("priority-popover-header")
|
|
230
|
+
expect(screen.queryByTestId("priority-overall-score")).toBeNull()
|
|
231
|
+
expect(header.textContent).toContain("Why this is high priority")
|
|
232
|
+
expect(header.textContent).not.toContain("79/100")
|
|
233
|
+
expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
|
|
234
|
+
expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
|
|
198
235
|
})
|
|
199
236
|
|
|
200
237
|
it("renders score track bars with correct width percentage", () => {
|
|
@@ -73,32 +73,6 @@ const approveReasons = [
|
|
|
73
73
|
|
|
74
74
|
type ApprovalState = "pending" | "confirming" | "creating" | "approving-feedback" | "dismissing" | "approved" | "dismissed" | "auto-approved"
|
|
75
75
|
|
|
76
|
-
interface OpportunityPreviewOption {
|
|
77
|
-
value: string
|
|
78
|
-
label: string
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface OpportunityPreview {
|
|
82
|
-
name: string
|
|
83
|
-
stage: string
|
|
84
|
-
closeDate: string
|
|
85
|
-
closeDateValue?: string
|
|
86
|
-
amount: string
|
|
87
|
-
/** Raw draft input value. Numeric values render as currency in the editable field. */
|
|
88
|
-
amountValue?: string | number | null
|
|
89
|
-
accountName: string
|
|
90
|
-
description?: string | null
|
|
91
|
-
churnType?: string | null
|
|
92
|
-
churnTypeOptions?: Array<string | OpportunityPreviewOption>
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface OpportunityDraft {
|
|
96
|
-
closeDate: string
|
|
97
|
-
amount: string
|
|
98
|
-
description: string
|
|
99
|
-
churnType: string
|
|
100
|
-
}
|
|
101
|
-
|
|
102
76
|
interface SignalApprovalLabels {
|
|
103
77
|
approveButton?: string
|
|
104
78
|
dismissButton?: string
|
|
@@ -132,9 +106,9 @@ interface SignalApprovalContextValue {
|
|
|
132
106
|
labels: Required<SignalApprovalLabels>
|
|
133
107
|
hideApproveButton?: boolean
|
|
134
108
|
approveButtonIconUrl?: string
|
|
135
|
-
opportunityPreview?:
|
|
109
|
+
opportunityPreview?: RootProps['opportunityPreview']
|
|
136
110
|
requestingApproval: boolean
|
|
137
|
-
approve: (
|
|
111
|
+
approve: () => void
|
|
138
112
|
submitApproveFeedback: (reasons: string[], detail: string) => void
|
|
139
113
|
skipApproveFeedback: () => void
|
|
140
114
|
dismiss: (reasons: string[], detail: string, subReason?: string) => void
|
|
@@ -163,7 +137,13 @@ interface RootProps {
|
|
|
163
137
|
/** Optional icon URL for the approve button. Renders an img instead of CirclePlus when provided. */
|
|
164
138
|
approveButtonIconUrl?: string
|
|
165
139
|
/** Optional structured preview data shown in the confirmation dialog. */
|
|
166
|
-
opportunityPreview?:
|
|
140
|
+
opportunityPreview?: {
|
|
141
|
+
name: string
|
|
142
|
+
stage: string
|
|
143
|
+
closeDate: string
|
|
144
|
+
amount: string
|
|
145
|
+
accountName: string
|
|
146
|
+
}
|
|
167
147
|
/**
|
|
168
148
|
* Async callback fired when the user clicks the approve button, BEFORE
|
|
169
149
|
* transitioning to the "confirming" state. While the promise is pending,
|
|
@@ -180,7 +160,7 @@ interface RootProps {
|
|
|
180
160
|
* "creating" loading state while the promise is pending. On `true` it
|
|
181
161
|
* transitions to the feedback step; on `false` it reverts to "pending".
|
|
182
162
|
*/
|
|
183
|
-
onApprove?: (
|
|
163
|
+
onApprove?: () => void | Promise<boolean>
|
|
184
164
|
onApproveFeedback?: (reasons: string[], detail: string) => void
|
|
185
165
|
onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
|
|
186
166
|
}
|
|
@@ -194,7 +174,6 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
194
174
|
// an async onApprove promise is still in flight).
|
|
195
175
|
const mountedRef = React.useRef(true)
|
|
196
176
|
React.useEffect(() => {
|
|
197
|
-
mountedRef.current = true
|
|
198
177
|
return () => { mountedRef.current = false }
|
|
199
178
|
}, [])
|
|
200
179
|
|
|
@@ -226,8 +205,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
226
205
|
setApprovalState("pending")
|
|
227
206
|
}, [])
|
|
228
207
|
|
|
229
|
-
const approve = React.useCallback((
|
|
230
|
-
const result = onApprove?.(
|
|
208
|
+
const approve = React.useCallback(() => {
|
|
209
|
+
const result = onApprove?.()
|
|
231
210
|
// If the callback returns a Promise, show a loading state and wait for it.
|
|
232
211
|
if (result && typeof (result as Promise<boolean>).then === "function") {
|
|
233
212
|
setApprovalState("creating")
|
|
@@ -438,49 +417,6 @@ function SubmittedFeedback({
|
|
|
438
417
|
)
|
|
439
418
|
}
|
|
440
419
|
|
|
441
|
-
function optionValue(option: string | OpportunityPreviewOption): string {
|
|
442
|
-
return typeof option === "string" ? option : option.value
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function optionLabel(option: string | OpportunityPreviewOption): string {
|
|
446
|
-
return typeof option === "string" ? option : option.label
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function formatAmountDraftValue(value: string | number | null | undefined): string {
|
|
450
|
-
if (value == null || value === "") return ""
|
|
451
|
-
if (typeof value === "number") {
|
|
452
|
-
return new Intl.NumberFormat("en-US", {
|
|
453
|
-
style: "currency",
|
|
454
|
-
currency: "USD",
|
|
455
|
-
maximumFractionDigits: 0,
|
|
456
|
-
}).format(value)
|
|
457
|
-
}
|
|
458
|
-
return value
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
|
|
462
|
-
return {
|
|
463
|
-
closeDate: preview?.closeDateValue ?? preview?.closeDate ?? "",
|
|
464
|
-
amount: preview?.amountValue === undefined
|
|
465
|
-
? preview?.amount ?? ""
|
|
466
|
-
: formatAmountDraftValue(preview.amountValue),
|
|
467
|
-
description: preview?.description ?? "",
|
|
468
|
-
churnType: preview?.churnType ?? "",
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function hasEditableOpportunityPreview(preview?: OpportunityPreview): boolean {
|
|
473
|
-
return !!preview && isValidDateInput(preview.closeDateValue ?? preview.closeDate)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function isValidDateInput(value: string): boolean {
|
|
477
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
|
478
|
-
if (!match) return false
|
|
479
|
-
const parsed = new Date(`${value}T00:00:00Z`)
|
|
480
|
-
if (Number.isNaN(parsed.getTime())) return false
|
|
481
|
-
return parsed.toISOString().slice(0, 10) === value
|
|
482
|
-
}
|
|
483
|
-
|
|
484
420
|
function Actions() {
|
|
485
421
|
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
486
422
|
useSignalApproval()
|
|
@@ -490,16 +426,6 @@ function Actions() {
|
|
|
490
426
|
const [detailText, setDetailText] = React.useState("")
|
|
491
427
|
const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
|
|
492
428
|
const [isEditing, setIsEditing] = React.useState(false)
|
|
493
|
-
const [opportunityDraft, setOpportunityDraft] = React.useState<OpportunityDraft>(() => buildOpportunityDraft(opportunityPreview))
|
|
494
|
-
|
|
495
|
-
React.useEffect(() => {
|
|
496
|
-
if (approvalState === "confirming") {
|
|
497
|
-
setOpportunityDraft(buildOpportunityDraft(opportunityPreview))
|
|
498
|
-
}
|
|
499
|
-
}, [approvalState, opportunityPreview])
|
|
500
|
-
|
|
501
|
-
const churnTypeOptions = opportunityPreview?.churnTypeOptions ?? []
|
|
502
|
-
const hasChurnTypeOptions = churnTypeOptions.length > 0
|
|
503
429
|
|
|
504
430
|
const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
|
|
505
431
|
const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
|
|
@@ -512,8 +438,6 @@ function Actions() {
|
|
|
512
438
|
(!needsText || detailText.trim().length > 0)
|
|
513
439
|
|
|
514
440
|
const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
|
|
515
|
-
const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
|
|
516
|
-
const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
|
|
517
441
|
|
|
518
442
|
const selectTopReason = (label: string) => {
|
|
519
443
|
if (selectedTopReason === label) {
|
|
@@ -815,8 +739,8 @@ function Actions() {
|
|
|
815
739
|
<p className="text-sm text-foreground">
|
|
816
740
|
{labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
|
|
817
741
|
</p>
|
|
818
|
-
{opportunityPreview &&
|
|
819
|
-
<div className="mt-3 space-y-
|
|
742
|
+
{opportunityPreview && (
|
|
743
|
+
<div className="mt-3 space-y-1.5 border-t border-border/50 pt-3">
|
|
820
744
|
{[
|
|
821
745
|
{ label: "Opportunity", value: opportunityPreview.name },
|
|
822
746
|
{ label: "Account", value: opportunityPreview.accountName },
|
|
@@ -824,105 +748,19 @@ function Actions() {
|
|
|
824
748
|
{ label: "Close Date", value: opportunityPreview.closeDate },
|
|
825
749
|
{ label: "Amount", value: opportunityPreview.amount },
|
|
826
750
|
].map(({ label, value }) => (
|
|
827
|
-
<div key={label} className="flex items-center justify-between
|
|
828
|
-
<span className="text-muted-foreground">{label}</span>
|
|
829
|
-
<span className="text-right font-medium text-foreground">{value}</span>
|
|
830
|
-
</div>
|
|
831
|
-
))}
|
|
832
|
-
</div>
|
|
833
|
-
)}
|
|
834
|
-
{opportunityPreview && isEditableOpportunityPreview && (
|
|
835
|
-
<div className="mt-3 space-y-3 border-t border-border/50 pt-3">
|
|
836
|
-
{[
|
|
837
|
-
{ label: "Opportunity", value: opportunityPreview.name },
|
|
838
|
-
{ label: "Account", value: opportunityPreview.accountName },
|
|
839
|
-
{ label: "Stage", value: opportunityPreview.stage },
|
|
840
|
-
].map(({ label, value }) => (
|
|
841
|
-
<div key={label} className="flex items-center justify-between gap-3 text-xs">
|
|
751
|
+
<div key={label} className="flex items-center justify-between text-xs">
|
|
842
752
|
<span className="text-muted-foreground">{label}</span>
|
|
843
|
-
<span className="
|
|
753
|
+
<span className="font-medium text-foreground">{value}</span>
|
|
844
754
|
</div>
|
|
845
755
|
))}
|
|
846
|
-
|
|
847
|
-
<div className="grid gap-2 sm:grid-cols-2">
|
|
848
|
-
<label className="space-y-1 text-xs">
|
|
849
|
-
<span className="font-medium text-muted-foreground">Close Date</span>
|
|
850
|
-
<input
|
|
851
|
-
type="date"
|
|
852
|
-
value={opportunityDraft.closeDate}
|
|
853
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, closeDate: event.target.value }))}
|
|
854
|
-
aria-invalid={!canConfirmOpportunity}
|
|
855
|
-
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
856
|
-
/>
|
|
857
|
-
{!canConfirmOpportunity && (
|
|
858
|
-
<span className="text-[11px] text-red-600">Enter a valid close date.</span>
|
|
859
|
-
)}
|
|
860
|
-
</label>
|
|
861
|
-
|
|
862
|
-
<label className="space-y-1 text-xs">
|
|
863
|
-
<span className="font-medium text-muted-foreground">Amount</span>
|
|
864
|
-
<input
|
|
865
|
-
type="text"
|
|
866
|
-
inputMode="decimal"
|
|
867
|
-
value={opportunityDraft.amount}
|
|
868
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, amount: event.target.value }))}
|
|
869
|
-
placeholder={opportunityPreview.amount}
|
|
870
|
-
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
871
|
-
/>
|
|
872
|
-
</label>
|
|
873
|
-
</div>
|
|
874
|
-
|
|
875
|
-
<label className="space-y-1 text-xs">
|
|
876
|
-
<span className="font-medium text-muted-foreground">Churn Type</span>
|
|
877
|
-
{hasChurnTypeOptions ? (
|
|
878
|
-
<select
|
|
879
|
-
value={opportunityDraft.churnType}
|
|
880
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
|
|
881
|
-
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
882
|
-
>
|
|
883
|
-
<option value="">No churn type</option>
|
|
884
|
-
{churnTypeOptions.map((option) => (
|
|
885
|
-
<option key={optionValue(option)} value={optionValue(option)}>
|
|
886
|
-
{optionLabel(option)}
|
|
887
|
-
</option>
|
|
888
|
-
))}
|
|
889
|
-
</select>
|
|
890
|
-
) : (
|
|
891
|
-
<input
|
|
892
|
-
type="text"
|
|
893
|
-
value={opportunityDraft.churnType}
|
|
894
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
|
|
895
|
-
placeholder="No churn type"
|
|
896
|
-
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
897
|
-
/>
|
|
898
|
-
)}
|
|
899
|
-
</label>
|
|
900
|
-
|
|
901
|
-
<label className="space-y-1 text-xs">
|
|
902
|
-
<span className="font-medium text-muted-foreground">Description</span>
|
|
903
|
-
<textarea
|
|
904
|
-
value={opportunityDraft.description}
|
|
905
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, description: event.target.value }))}
|
|
906
|
-
rows={3}
|
|
907
|
-
placeholder="Add a short description"
|
|
908
|
-
className="w-full resize-none rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
909
|
-
/>
|
|
910
|
-
</label>
|
|
911
756
|
</div>
|
|
912
757
|
)}
|
|
913
758
|
</div>
|
|
914
759
|
<div className="flex items-center gap-2">
|
|
915
760
|
<button
|
|
916
761
|
type="button"
|
|
917
|
-
onClick={
|
|
918
|
-
|
|
919
|
-
approve()
|
|
920
|
-
return
|
|
921
|
-
}
|
|
922
|
-
approve(isEditableOpportunityPreview ? opportunityDraft : undefined)
|
|
923
|
-
}}
|
|
924
|
-
disabled={!canConfirmOpportunity}
|
|
925
|
-
className="inline-flex h-7 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-semibold text-background transition-colors hover:bg-foreground/90 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground"
|
|
762
|
+
onClick={approve}
|
|
763
|
+
className="inline-flex h-7 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-semibold text-background transition-colors hover:bg-foreground/90"
|
|
926
764
|
>
|
|
927
765
|
<Check className="h-3 w-3" />
|
|
928
766
|
Confirm
|
|
@@ -1023,4 +861,5 @@ export {
|
|
|
1023
861
|
Gate as SignalApprovalGate,
|
|
1024
862
|
}
|
|
1025
863
|
export const SignalApproval = { Root, Actions, Gate }
|
|
1026
|
-
export type
|
|
864
|
+
export type OpportunityPreview = NonNullable<RootProps['opportunityPreview']>
|
|
865
|
+
export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
@@ -50,8 +50,14 @@ export interface PriorityFactor {
|
|
|
50
50
|
rationale: string
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export type SignalPriorityScoreDisplay = "label" | "number"
|
|
54
|
+
|
|
53
55
|
export interface SignalPriorityPopoverProps {
|
|
54
56
|
score: number
|
|
57
|
+
/** Controls whether the overall score number is shown in the popover header. @default "number" */
|
|
58
|
+
scoreDisplay?: SignalPriorityScoreDisplay
|
|
59
|
+
/** Short formula/context label shown beside the contributing factors heading. @default "Priority factors" */
|
|
60
|
+
formulaLabel?: string
|
|
55
61
|
urgencyLabel?: SignalScoreUrgencyLabel
|
|
56
62
|
/** Synthesis sentence displayed in the popover head. */
|
|
57
63
|
urgencyExplanation?: string
|
|
@@ -282,6 +288,8 @@ export function SignalPriorityPopover({
|
|
|
282
288
|
initialFactorFeedback,
|
|
283
289
|
onFactorFeedback,
|
|
284
290
|
initialPriorityFeedback,
|
|
291
|
+
scoreDisplay = "number",
|
|
292
|
+
formulaLabel = "Priority factors",
|
|
285
293
|
}: SignalPriorityPopoverProps) {
|
|
286
294
|
const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
|
|
287
295
|
const scoreRange = getUrgencyRange(urgencyLabel)
|
|
@@ -330,15 +338,20 @@ export function SignalPriorityPopover({
|
|
|
330
338
|
data-testid="priority-popover-content"
|
|
331
339
|
>
|
|
332
340
|
{/* Head section */}
|
|
333
|
-
<div className="p-4 pb-3">
|
|
341
|
+
<div className="p-4 pb-3" data-testid="priority-popover-header">
|
|
334
342
|
<div className="flex items-start justify-between gap-3">
|
|
335
343
|
<p className="text-sm font-semibold text-foreground">
|
|
336
344
|
Why this is {urgencyLabel.toLowerCase()} priority
|
|
337
345
|
</p>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
346
|
+
{scoreDisplay === "number" && (
|
|
347
|
+
<span
|
|
348
|
+
className="text-2xl font-bold tabular-nums text-foreground"
|
|
349
|
+
data-testid="priority-overall-score"
|
|
350
|
+
>
|
|
351
|
+
{score}
|
|
352
|
+
<span className="text-sm font-normal text-muted-foreground">/100</span>
|
|
353
|
+
</span>
|
|
354
|
+
)}
|
|
342
355
|
</div>
|
|
343
356
|
|
|
344
357
|
{/* Band indicator */}
|
|
@@ -369,7 +382,7 @@ export function SignalPriorityPopover({
|
|
|
369
382
|
</span>
|
|
370
383
|
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
371
384
|
<Info className="h-3 w-3" />
|
|
372
|
-
|
|
385
|
+
{formulaLabel}
|
|
373
386
|
</span>
|
|
374
387
|
</div>
|
|
375
388
|
|
package/src/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ export * from "./components/related-record-action-card"
|
|
|
50
50
|
export { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from "./components/feedback-primitives"
|
|
51
51
|
export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from "./components/feedback-primitives"
|
|
52
52
|
export { SignalPriorityPopover } from "./components/signal-priority-popover"
|
|
53
|
-
export type { SignalPriorityPopoverProps, PriorityFactor } from "./components/signal-priority-popover"
|
|
53
|
+
export type { SignalPriorityPopoverProps, SignalPriorityScoreDisplay, PriorityFactor } from "./components/signal-priority-popover"
|
|
54
54
|
export * from "./components/filter-chip"
|
|
55
55
|
export * from "./components/inbox-row"
|
|
56
56
|
export * from "./components/inbox-toolbar"
|
|
@@ -89,6 +89,40 @@ describe("DetailView corrected compact score WHY UX", () => {
|
|
|
89
89
|
expect(screen.queryByText("How's this score?")).toBeNull();
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
|
|
93
|
+
it("passes priorityScoreDisplay and priorityFormulaLabel through to the priority popover", () => {
|
|
94
|
+
render(
|
|
95
|
+
<DetailView
|
|
96
|
+
{...baseProps({
|
|
97
|
+
getSignalScore: () =>
|
|
98
|
+
makeSignalScore({
|
|
99
|
+
urgencyLabel: "High",
|
|
100
|
+
priorityScoreDisplay: "label",
|
|
101
|
+
priorityFormulaLabel: "Priority = weighted signals + calibration",
|
|
102
|
+
priorityFactors: [
|
|
103
|
+
{
|
|
104
|
+
key: "treasury",
|
|
105
|
+
label: "Treasury activity",
|
|
106
|
+
icon: "wallet",
|
|
107
|
+
tone: "alert",
|
|
108
|
+
direction: "raises",
|
|
109
|
+
score: 85,
|
|
110
|
+
rationale: "Full liquidation detected.",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
})}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
fireEvent.click(screen.getByRole("button", { name: /high priority/i }));
|
|
119
|
+
|
|
120
|
+
expect(screen.queryByTestId("priority-overall-score")).toBeNull();
|
|
121
|
+
expect(screen.getByTestId("priority-popover-header").textContent).not.toContain("82/100");
|
|
122
|
+
expect(screen.getByText("Priority = weighted signals + calibration")).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByTestId("factor-row-treasury").textContent).toContain("85/100");
|
|
124
|
+
});
|
|
125
|
+
|
|
92
126
|
it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
|
|
93
127
|
render(
|
|
94
128
|
<DetailView
|
|
@@ -13,9 +13,9 @@ import type {
|
|
|
13
13
|
PipelineStageTiming,
|
|
14
14
|
} from "../charts/pipeline-overview"
|
|
15
15
|
import type { TimelineEvent } from "../components/timeline-activity"
|
|
16
|
-
import type { ApprovalState
|
|
16
|
+
import type { ApprovalState } from "../components/signal-feedback-inline"
|
|
17
17
|
import type { LucideIcon } from "lucide-react"
|
|
18
|
-
import type { PriorityFactor } from "../components/signal-priority-popover"
|
|
18
|
+
import type { PriorityFactor, SignalPriorityScoreDisplay } from "../components/signal-priority-popover"
|
|
19
19
|
import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -109,6 +109,10 @@ export interface SignalScoreData {
|
|
|
109
109
|
confidence: number
|
|
110
110
|
urgencyLabel?: SignalScoreUrgencyLabel
|
|
111
111
|
urgencyExplanation?: string
|
|
112
|
+
/** Controls whether the priority popover header shows the raw overall score number. @default "number" */
|
|
113
|
+
priorityScoreDisplay?: SignalPriorityScoreDisplay
|
|
114
|
+
/** Formula/context label shown in the priority popover factor section. @default "Priority factors" */
|
|
115
|
+
priorityFormulaLabel?: string
|
|
112
116
|
explanationBuckets?: SignalScoreExplanationBucket[]
|
|
113
117
|
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
|
|
114
118
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
@@ -180,7 +184,7 @@ export interface InboxViewConfig {
|
|
|
180
184
|
quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
|
|
181
185
|
hideAccountsButton?: boolean
|
|
182
186
|
accountDetailsLabel?: string
|
|
183
|
-
onSignalApprove?: (item: QueueItem
|
|
187
|
+
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
184
188
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
185
189
|
signalLabels?: {
|
|
186
190
|
approveButton?: string
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type InboxFilterCategory,
|
|
39
39
|
} from "../components/inbox-toolbar"
|
|
40
40
|
import { GroupedListView, type GroupedListGroup } from "../components/item-list"
|
|
41
|
-
import { SignalApproval, type ApprovalState, type
|
|
41
|
+
import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
|
|
42
42
|
import { ScoreWhyChips } from "../components/score-why-chips"
|
|
43
43
|
import { SignalPriorityPopover } from "../components/signal-priority-popover"
|
|
44
44
|
import { type SourceDef } from "../components/detail-view"
|
|
@@ -149,7 +149,7 @@ export interface DetailViewProps {
|
|
|
149
149
|
onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
|
|
150
150
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
151
151
|
onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
152
|
-
onSignalApprove?: (item: QueueItem
|
|
152
|
+
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
153
153
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
154
154
|
signalLabels?: InboxViewConfig["signalLabels"]
|
|
155
155
|
hideApproveButton?: boolean
|
|
@@ -475,7 +475,7 @@ export function DetailView({
|
|
|
475
475
|
opportunityPreview={opportunityPreview}
|
|
476
476
|
onRequestApproval={onRequestApproval}
|
|
477
477
|
initialApprovalState={getSignalApprovalState?.(item)}
|
|
478
|
-
onApprove={(
|
|
478
|
+
onApprove={() => onSignalApprove?.(item)}
|
|
479
479
|
onApproveFeedback={(reasons, detail) => {
|
|
480
480
|
signalData.onApproveFeedback?.(reasons, detail)
|
|
481
481
|
console.log("Approval feedback:", { taskId: item.id, company: item.company, reasons, detail })
|
|
@@ -519,6 +519,8 @@ export function DetailView({
|
|
|
519
519
|
score={signalData.score}
|
|
520
520
|
urgencyLabel={signalData.urgencyLabel}
|
|
521
521
|
urgencyExplanation={signalData.urgencyExplanation ?? signalData.signalBrief}
|
|
522
|
+
scoreDisplay={signalData.priorityScoreDisplay}
|
|
523
|
+
formulaLabel={signalData.priorityFormulaLabel}
|
|
522
524
|
factors={signalData.priorityFactors ?? []}
|
|
523
525
|
metaText={undefined}
|
|
524
526
|
feedbackChips={signalData.priorityFeedbackChips}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import { describe, expect, it } from "vitest"
|
|
3
|
-
import { fireEvent, render, screen } from "@testing-library/react"
|
|
4
|
-
|
|
5
|
-
import { DetailView, type DetailViewProps } from "../prototype-inbox-view"
|
|
6
|
-
import type { QueueItem, SignalScoreData } from "../prototype-config"
|
|
7
|
-
|
|
8
|
-
const baseItem: QueueItem = {
|
|
9
|
-
id: "case-1",
|
|
10
|
-
title: "WIT-825 Opportunity Preview Fixture",
|
|
11
|
-
details: "Some details",
|
|
12
|
-
statusColor: "green",
|
|
13
|
-
time: "2h ago",
|
|
14
|
-
company: "WIT-825 Fixture Account",
|
|
15
|
-
tag1: "churn_risk",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function makeSignalScore(overrides: Partial<SignalScoreData> = {}): SignalScoreData {
|
|
19
|
-
return {
|
|
20
|
-
score: 82,
|
|
21
|
-
factors: [],
|
|
22
|
-
whyNow: "Strong signals detected.",
|
|
23
|
-
evidence: ["Evidence line 1"],
|
|
24
|
-
confidence: 80,
|
|
25
|
-
signalBrief: "Signals indicate a potential opportunity.",
|
|
26
|
-
...overrides,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function baseProps(overrides: Partial<DetailViewProps> = {}): DetailViewProps {
|
|
31
|
-
return {
|
|
32
|
-
item: baseItem,
|
|
33
|
-
sections: { signalBrief: true, suggestedActions: false, timeline: false },
|
|
34
|
-
getSignalScore: () => makeSignalScore(),
|
|
35
|
-
buildSuggestedActions: () => [],
|
|
36
|
-
buildSourceItems: () => [],
|
|
37
|
-
accountContacts: [],
|
|
38
|
-
emailSignature: "",
|
|
39
|
-
iconMap: {},
|
|
40
|
-
signalLabels: {
|
|
41
|
-
approveButton: "Create Opportunity",
|
|
42
|
-
dismissButton: "Not Helpful",
|
|
43
|
-
},
|
|
44
|
-
...overrides,
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe("DetailView opportunity approval preview", () => {
|
|
49
|
-
it("keeps the approval flow mounted and shows editable preview fields after async request approval in StrictMode", async () => {
|
|
50
|
-
function DetailViewPreviewHarness() {
|
|
51
|
-
const [opportunityPreview, setOpportunityPreview] = React.useState<DetailViewProps["opportunityPreview"]>()
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<DetailView
|
|
55
|
-
{...baseProps({
|
|
56
|
-
opportunityPreview,
|
|
57
|
-
onRequestApproval: async () => {
|
|
58
|
-
setOpportunityPreview({
|
|
59
|
-
name: "Churn Risk - WIT-825 Fixture Account",
|
|
60
|
-
accountName: "WIT-825 Fixture Account",
|
|
61
|
-
stage: "Prospecting",
|
|
62
|
-
closeDate: "Jun 30, 2026",
|
|
63
|
-
closeDateValue: "2026-06-30",
|
|
64
|
-
amount: "$75,000",
|
|
65
|
-
amountValue: 75000,
|
|
66
|
-
description: "Initial description",
|
|
67
|
-
churnType: "Churn Risk",
|
|
68
|
-
churnTypeOptions: ["Churn Risk", "Win Back"],
|
|
69
|
-
})
|
|
70
|
-
},
|
|
71
|
-
})}
|
|
72
|
-
/>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
render(
|
|
77
|
-
<React.StrictMode>
|
|
78
|
-
<DetailViewPreviewHarness />
|
|
79
|
-
</React.StrictMode>,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
fireEvent.click(screen.getByRole("button", { name: /create opportunity/i }))
|
|
83
|
-
|
|
84
|
-
expect((await screen.findByLabelText("Close Date") as HTMLInputElement).value).toBe("2026-06-30")
|
|
85
|
-
expect((screen.getByLabelText("Amount") as HTMLInputElement).value).toBe("$75,000")
|
|
86
|
-
expect((screen.getByLabelText("Churn Type") as HTMLSelectElement).value).toBe("Churn Risk")
|
|
87
|
-
expect((screen.getByLabelText("Description") as HTMLTextAreaElement).value).toBe("Initial description")
|
|
88
|
-
expect((screen.getByRole("button", { name: /confirm/i }) as HTMLButtonElement).disabled).toBe(false)
|
|
89
|
-
})
|
|
90
|
-
})
|