@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.
Files changed (27) hide show
  1. package/dist/components/score-why-chips.d.ts +1 -1
  2. package/dist/components/signal-feedback-inline.d.ts +12 -28
  3. package/dist/components/signal-feedback-inline.js +10 -146
  4. package/dist/components/signal-feedback-inline.js.map +1 -1
  5. package/dist/components/signal-priority-popover.d.ts +1 -1
  6. package/dist/components/signal-priority-popover.js +15 -7
  7. package/dist/components/signal-priority-popover.js.map +1 -1
  8. package/dist/index.d.ts +2 -2
  9. package/dist/prototype/index.d.ts +1 -1
  10. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  11. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  12. package/dist/prototype/prototype-config.d.ts +1 -1
  13. package/dist/prototype/prototype-inbox-view.d.ts +3 -3
  14. package/dist/prototype/prototype-inbox-view.js +2 -1
  15. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  16. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  17. package/dist/prototype/prototype-shell.d.ts +1 -1
  18. package/dist/{signal-priority-popover-QJngMAj7.d.ts → signal-priority-popover-Cl98xw1n.d.ts} +9 -4
  19. package/package.json +1 -1
  20. package/src/components/__tests__/case-panel-why.test.tsx +0 -126
  21. package/src/components/__tests__/signal-priority-popover.test.tsx +27 -4
  22. package/src/components/signal-feedback-inline.tsx +20 -181
  23. package/src/components/signal-priority-popover.tsx +16 -6
  24. package/src/prototype/__tests__/detail-view-score-why.test.tsx +32 -0
  25. package/src/prototype/prototype-config.ts +5 -3
  26. package/src/prototype/prototype-inbox-view.tsx +4 -3
  27. 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?: OpportunityPreview
109
+ opportunityPreview?: RootProps['opportunityPreview']
136
110
  requestingApproval: boolean
137
- approve: (draft?: OpportunityDraft) => void
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?: 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?: (draft?: OpportunityDraft) => void | Promise<boolean>
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((draft?: OpportunityDraft) => {
230
- const result = onApprove?.(draft)
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 && !isEditableOpportunityPreview && (
819
- <div className="mt-3 space-y-2 border-t border-border/50 pt-3">
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 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">
751
+ <div key={label} className="flex items-center justify-between text-xs">
842
752
  <span className="text-muted-foreground">{label}</span>
843
- <span className="text-right font-medium text-foreground">{value}</span>
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
- 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"
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 { ApprovalState, OpportunityPreview, OpportunityDraft, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
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
- <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>
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
- Score = weighted sum
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, OpportunityDraft } from "../components/signal-feedback-inline"
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, draft?: OpportunityDraft) => void | Promise<boolean>
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 OpportunityDraft, type OpportunityPreview } from "../components/signal-feedback-inline"
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, draft?: OpportunityDraft) => void | Promise<boolean>
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={(draft) => onSignalApprove?.(item, draft)}
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
- })