@handled-ai/design-system 0.18.51 → 0.18.52
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/data-table-filter.d.ts +21 -6
- package/dist/components/data-table-filter.js +134 -9
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-feedback-inline.d.ts +28 -12
- package/dist/components/signal-feedback-inline.js +146 -10
- 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 +7 -16
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/index.d.ts +3 -3
- 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 +1 -3
- 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-DTedstRL.d.ts → signal-priority-popover-QJngMAj7.d.ts} +4 -13
- package/package.json +1 -1
- package/src/components/__tests__/case-panel-why.test.tsx +126 -0
- package/src/components/__tests__/data-table-filter.test.tsx +130 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +4 -41
- package/src/components/data-table-filter.tsx +160 -9
- package/src/components/signal-feedback-inline.tsx +181 -20
- package/src/components/signal-priority-popover.tsx +6 -19
- package/src/index.ts +1 -1
- package/src/prototype/__tests__/detail-view-opportunity-preview.test.tsx +90 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +0 -34
- package/src/prototype/prototype-config.ts +3 -7
- package/src/prototype/prototype-inbox-view.tsx +3 -5
|
@@ -73,6 +73,32 @@ 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
|
+
|
|
76
102
|
interface SignalApprovalLabels {
|
|
77
103
|
approveButton?: string
|
|
78
104
|
dismissButton?: string
|
|
@@ -106,9 +132,9 @@ interface SignalApprovalContextValue {
|
|
|
106
132
|
labels: Required<SignalApprovalLabels>
|
|
107
133
|
hideApproveButton?: boolean
|
|
108
134
|
approveButtonIconUrl?: string
|
|
109
|
-
opportunityPreview?:
|
|
135
|
+
opportunityPreview?: OpportunityPreview
|
|
110
136
|
requestingApproval: boolean
|
|
111
|
-
approve: () => void
|
|
137
|
+
approve: (draft?: OpportunityDraft) => void
|
|
112
138
|
submitApproveFeedback: (reasons: string[], detail: string) => void
|
|
113
139
|
skipApproveFeedback: () => void
|
|
114
140
|
dismiss: (reasons: string[], detail: string, subReason?: string) => void
|
|
@@ -137,13 +163,7 @@ interface RootProps {
|
|
|
137
163
|
/** Optional icon URL for the approve button. Renders an img instead of CirclePlus when provided. */
|
|
138
164
|
approveButtonIconUrl?: string
|
|
139
165
|
/** Optional structured preview data shown in the confirmation dialog. */
|
|
140
|
-
opportunityPreview?:
|
|
141
|
-
name: string
|
|
142
|
-
stage: string
|
|
143
|
-
closeDate: string
|
|
144
|
-
amount: string
|
|
145
|
-
accountName: string
|
|
146
|
-
}
|
|
166
|
+
opportunityPreview?: OpportunityPreview
|
|
147
167
|
/**
|
|
148
168
|
* Async callback fired when the user clicks the approve button, BEFORE
|
|
149
169
|
* transitioning to the "confirming" state. While the promise is pending,
|
|
@@ -160,7 +180,7 @@ interface RootProps {
|
|
|
160
180
|
* "creating" loading state while the promise is pending. On `true` it
|
|
161
181
|
* transitions to the feedback step; on `false` it reverts to "pending".
|
|
162
182
|
*/
|
|
163
|
-
onApprove?: () => void | Promise<boolean>
|
|
183
|
+
onApprove?: (draft?: OpportunityDraft) => void | Promise<boolean>
|
|
164
184
|
onApproveFeedback?: (reasons: string[], detail: string) => void
|
|
165
185
|
onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
|
|
166
186
|
}
|
|
@@ -174,6 +194,7 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
174
194
|
// an async onApprove promise is still in flight).
|
|
175
195
|
const mountedRef = React.useRef(true)
|
|
176
196
|
React.useEffect(() => {
|
|
197
|
+
mountedRef.current = true
|
|
177
198
|
return () => { mountedRef.current = false }
|
|
178
199
|
}, [])
|
|
179
200
|
|
|
@@ -205,8 +226,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
205
226
|
setApprovalState("pending")
|
|
206
227
|
}, [])
|
|
207
228
|
|
|
208
|
-
const approve = React.useCallback(() => {
|
|
209
|
-
const result = onApprove?.()
|
|
229
|
+
const approve = React.useCallback((draft?: OpportunityDraft) => {
|
|
230
|
+
const result = onApprove?.(draft)
|
|
210
231
|
// If the callback returns a Promise, show a loading state and wait for it.
|
|
211
232
|
if (result && typeof (result as Promise<boolean>).then === "function") {
|
|
212
233
|
setApprovalState("creating")
|
|
@@ -417,6 +438,49 @@ function SubmittedFeedback({
|
|
|
417
438
|
)
|
|
418
439
|
}
|
|
419
440
|
|
|
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
|
+
|
|
420
484
|
function Actions() {
|
|
421
485
|
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
422
486
|
useSignalApproval()
|
|
@@ -426,6 +490,16 @@ function Actions() {
|
|
|
426
490
|
const [detailText, setDetailText] = React.useState("")
|
|
427
491
|
const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
|
|
428
492
|
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
|
|
429
503
|
|
|
430
504
|
const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
|
|
431
505
|
const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
|
|
@@ -438,6 +512,8 @@ function Actions() {
|
|
|
438
512
|
(!needsText || detailText.trim().length > 0)
|
|
439
513
|
|
|
440
514
|
const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
|
|
515
|
+
const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
|
|
516
|
+
const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
|
|
441
517
|
|
|
442
518
|
const selectTopReason = (label: string) => {
|
|
443
519
|
if (selectedTopReason === label) {
|
|
@@ -739,8 +815,8 @@ function Actions() {
|
|
|
739
815
|
<p className="text-sm text-foreground">
|
|
740
816
|
{labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
|
|
741
817
|
</p>
|
|
742
|
-
{opportunityPreview && (
|
|
743
|
-
<div className="mt-3 space-y-
|
|
818
|
+
{opportunityPreview && !isEditableOpportunityPreview && (
|
|
819
|
+
<div className="mt-3 space-y-2 border-t border-border/50 pt-3">
|
|
744
820
|
{[
|
|
745
821
|
{ label: "Opportunity", value: opportunityPreview.name },
|
|
746
822
|
{ label: "Account", value: opportunityPreview.accountName },
|
|
@@ -748,19 +824,105 @@ function Actions() {
|
|
|
748
824
|
{ label: "Close Date", value: opportunityPreview.closeDate },
|
|
749
825
|
{ label: "Amount", value: opportunityPreview.amount },
|
|
750
826
|
].map(({ label, value }) => (
|
|
751
|
-
<div key={label} className="flex items-center justify-between text-xs">
|
|
827
|
+
<div key={label} className="flex items-center justify-between gap-3 text-xs">
|
|
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">
|
|
752
842
|
<span className="text-muted-foreground">{label}</span>
|
|
753
|
-
<span className="font-medium text-foreground">{value}</span>
|
|
843
|
+
<span className="text-right font-medium text-foreground">{value}</span>
|
|
754
844
|
</div>
|
|
755
845
|
))}
|
|
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>
|
|
756
911
|
</div>
|
|
757
912
|
)}
|
|
758
913
|
</div>
|
|
759
914
|
<div className="flex items-center gap-2">
|
|
760
915
|
<button
|
|
761
916
|
type="button"
|
|
762
|
-
onClick={
|
|
763
|
-
|
|
917
|
+
onClick={() => {
|
|
918
|
+
if (!opportunityPreview) {
|
|
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"
|
|
764
926
|
>
|
|
765
927
|
<Check className="h-3 w-3" />
|
|
766
928
|
Confirm
|
|
@@ -861,5 +1023,4 @@ export {
|
|
|
861
1023
|
Gate as SignalApprovalGate,
|
|
862
1024
|
}
|
|
863
1025
|
export const SignalApproval = { Root, Actions, Gate }
|
|
864
|
-
export type OpportunityPreview
|
|
865
|
-
export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
1026
|
+
export type { ApprovalState, OpportunityPreview, OpportunityDraft, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
@@ -50,14 +50,8 @@ export interface PriorityFactor {
|
|
|
50
50
|
rationale: string
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export type SignalPriorityScoreDisplay = "label" | "number"
|
|
54
|
-
|
|
55
53
|
export interface SignalPriorityPopoverProps {
|
|
56
54
|
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
|
|
61
55
|
urgencyLabel?: SignalScoreUrgencyLabel
|
|
62
56
|
/** Synthesis sentence displayed in the popover head. */
|
|
63
57
|
urgencyExplanation?: string
|
|
@@ -288,8 +282,6 @@ export function SignalPriorityPopover({
|
|
|
288
282
|
initialFactorFeedback,
|
|
289
283
|
onFactorFeedback,
|
|
290
284
|
initialPriorityFeedback,
|
|
291
|
-
scoreDisplay = "number",
|
|
292
|
-
formulaLabel = "Priority factors",
|
|
293
285
|
}: SignalPriorityPopoverProps) {
|
|
294
286
|
const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
|
|
295
287
|
const scoreRange = getUrgencyRange(urgencyLabel)
|
|
@@ -338,20 +330,15 @@ export function SignalPriorityPopover({
|
|
|
338
330
|
data-testid="priority-popover-content"
|
|
339
331
|
>
|
|
340
332
|
{/* Head section */}
|
|
341
|
-
<div className="p-4 pb-3"
|
|
333
|
+
<div className="p-4 pb-3">
|
|
342
334
|
<div className="flex items-start justify-between gap-3">
|
|
343
335
|
<p className="text-sm font-semibold text-foreground">
|
|
344
336
|
Why this is {urgencyLabel.toLowerCase()} priority
|
|
345
337
|
</p>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
>
|
|
351
|
-
{score}
|
|
352
|
-
<span className="text-sm font-normal text-muted-foreground">/100</span>
|
|
353
|
-
</span>
|
|
354
|
-
)}
|
|
338
|
+
<span className="text-2xl font-bold tabular-nums text-foreground">
|
|
339
|
+
{score}
|
|
340
|
+
<span className="text-sm font-normal text-muted-foreground">/100</span>
|
|
341
|
+
</span>
|
|
355
342
|
</div>
|
|
356
343
|
|
|
357
344
|
{/* Band indicator */}
|
|
@@ -382,7 +369,7 @@ export function SignalPriorityPopover({
|
|
|
382
369
|
</span>
|
|
383
370
|
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
384
371
|
<Info className="h-3 w-3" />
|
|
385
|
-
|
|
372
|
+
Score = weighted sum
|
|
386
373
|
</span>
|
|
387
374
|
</div>
|
|
388
375
|
|
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,
|
|
53
|
+
export type { SignalPriorityPopoverProps, 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"
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
})
|
|
@@ -89,40 +89,6 @@ 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
|
-
|
|
126
92
|
it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
|
|
127
93
|
render(
|
|
128
94
|
<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 } from "../components/signal-feedback-inline"
|
|
16
|
+
import type { ApprovalState, OpportunityDraft } from "../components/signal-feedback-inline"
|
|
17
17
|
import type { LucideIcon } from "lucide-react"
|
|
18
|
-
import type { PriorityFactor
|
|
18
|
+
import type { PriorityFactor } from "../components/signal-priority-popover"
|
|
19
19
|
import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -109,10 +109,6 @@ 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
|
|
116
112
|
explanationBuckets?: SignalScoreExplanationBucket[]
|
|
117
113
|
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
|
|
118
114
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
@@ -184,7 +180,7 @@ export interface InboxViewConfig {
|
|
|
184
180
|
quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
|
|
185
181
|
hideAccountsButton?: boolean
|
|
186
182
|
accountDetailsLabel?: string
|
|
187
|
-
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
183
|
+
onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
|
|
188
184
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
189
185
|
signalLabels?: {
|
|
190
186
|
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 OpportunityPreview } from "../components/signal-feedback-inline"
|
|
41
|
+
import { SignalApproval, type ApprovalState, type OpportunityDraft, 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) => void | Promise<boolean>
|
|
152
|
+
onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => 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={() => onSignalApprove?.(item)}
|
|
478
|
+
onApprove={(draft) => onSignalApprove?.(item, draft)}
|
|
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,8 +519,6 @@ 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}
|
|
524
522
|
factors={signalData.priorityFactors ?? []}
|
|
525
523
|
metaText={undefined}
|
|
526
524
|
feedbackChips={signalData.priorityFeedbackChips}
|