@handled-ai/design-system 0.18.49 → 0.18.50
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/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 +15 -7
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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 +2 -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-Cl98xw1n.d.ts} +9 -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 +27 -4
- package/src/components/signal-feedback-inline.tsx +20 -181
- package/src/components/signal-priority-popover.tsx +16 -6
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +32 -0
- package/src/prototype/prototype-config.ts +5 -3
- package/src/prototype/prototype-inbox-view.tsx +4 -3
- package/src/prototype/__tests__/detail-view-opportunity-preview.test.tsx +0 -90
|
@@ -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,12 @@ 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
|
|
55
59
|
urgencyLabel?: SignalScoreUrgencyLabel
|
|
56
60
|
/** Synthesis sentence displayed in the popover head. */
|
|
57
61
|
urgencyExplanation?: string
|
|
@@ -282,6 +286,7 @@ export function SignalPriorityPopover({
|
|
|
282
286
|
initialFactorFeedback,
|
|
283
287
|
onFactorFeedback,
|
|
284
288
|
initialPriorityFeedback,
|
|
289
|
+
scoreDisplay = "number",
|
|
285
290
|
}: SignalPriorityPopoverProps) {
|
|
286
291
|
const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
|
|
287
292
|
const scoreRange = getUrgencyRange(urgencyLabel)
|
|
@@ -330,15 +335,20 @@ export function SignalPriorityPopover({
|
|
|
330
335
|
data-testid="priority-popover-content"
|
|
331
336
|
>
|
|
332
337
|
{/* Head section */}
|
|
333
|
-
<div className="p-4 pb-3">
|
|
338
|
+
<div className="p-4 pb-3" data-testid="priority-popover-header">
|
|
334
339
|
<div className="flex items-start justify-between gap-3">
|
|
335
340
|
<p className="text-sm font-semibold text-foreground">
|
|
336
341
|
Why this is {urgencyLabel.toLowerCase()} priority
|
|
337
342
|
</p>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
{scoreDisplay === "number" && (
|
|
344
|
+
<span
|
|
345
|
+
className="text-2xl font-bold tabular-nums text-foreground"
|
|
346
|
+
data-testid="priority-overall-score"
|
|
347
|
+
>
|
|
348
|
+
{score}
|
|
349
|
+
<span className="text-sm font-normal text-muted-foreground">/100</span>
|
|
350
|
+
</span>
|
|
351
|
+
)}
|
|
342
352
|
</div>
|
|
343
353
|
|
|
344
354
|
{/* Band indicator */}
|
|
@@ -369,7 +379,7 @@ export function SignalPriorityPopover({
|
|
|
369
379
|
</span>
|
|
370
380
|
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
371
381
|
<Info className="h-3 w-3" />
|
|
372
|
-
|
|
382
|
+
Priority = weighted signals + calibration
|
|
373
383
|
</span>
|
|
374
384
|
</div>
|
|
375
385
|
|
|
@@ -89,6 +89,38 @@ 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 through to the priority popover", () => {
|
|
94
|
+
render(
|
|
95
|
+
<DetailView
|
|
96
|
+
{...baseProps({
|
|
97
|
+
getSignalScore: () =>
|
|
98
|
+
makeSignalScore({
|
|
99
|
+
urgencyLabel: "High",
|
|
100
|
+
priorityScoreDisplay: "label",
|
|
101
|
+
priorityFactors: [
|
|
102
|
+
{
|
|
103
|
+
key: "treasury",
|
|
104
|
+
label: "Treasury activity",
|
|
105
|
+
icon: "wallet",
|
|
106
|
+
tone: "alert",
|
|
107
|
+
direction: "raises",
|
|
108
|
+
score: 85,
|
|
109
|
+
rationale: "Full liquidation detected.",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
})}
|
|
114
|
+
/>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
fireEvent.click(screen.getByRole("button", { name: /high priority/i }));
|
|
118
|
+
|
|
119
|
+
expect(screen.queryByTestId("priority-overall-score")).toBeNull();
|
|
120
|
+
expect(screen.getByTestId("priority-popover-header").textContent).not.toContain("82/100");
|
|
121
|
+
expect(screen.getByTestId("factor-row-treasury").textContent).toContain("85/100");
|
|
122
|
+
});
|
|
123
|
+
|
|
92
124
|
it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
|
|
93
125
|
render(
|
|
94
126
|
<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,8 @@ 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
|
|
112
114
|
explanationBuckets?: SignalScoreExplanationBucket[]
|
|
113
115
|
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
|
|
114
116
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
@@ -180,7 +182,7 @@ export interface InboxViewConfig {
|
|
|
180
182
|
quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
|
|
181
183
|
hideAccountsButton?: boolean
|
|
182
184
|
accountDetailsLabel?: string
|
|
183
|
-
onSignalApprove?: (item: QueueItem
|
|
185
|
+
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
184
186
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
185
187
|
signalLabels?: {
|
|
186
188
|
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,7 @@ export function DetailView({
|
|
|
519
519
|
score={signalData.score}
|
|
520
520
|
urgencyLabel={signalData.urgencyLabel}
|
|
521
521
|
urgencyExplanation={signalData.urgencyExplanation ?? signalData.signalBrief}
|
|
522
|
+
scoreDisplay={signalData.priorityScoreDisplay}
|
|
522
523
|
factors={signalData.priorityFactors ?? []}
|
|
523
524
|
metaText={undefined}
|
|
524
525
|
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
|
-
})
|