@handled-ai/design-system 0.18.48 → 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 -145
- 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 -82
- package/src/components/__tests__/signal-priority-popover.test.tsx +27 -4
- package/src/components/signal-feedback-inline.tsx +20 -180
- 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/dist/{signal-priority-popover-QJngMAj7.d.ts → signal-priority-popover-Cl98xw1n.d.ts}
RENAMED
|
@@ -10,7 +10,7 @@ import { DataRow } from './components/data-table.js';
|
|
|
10
10
|
import { MetricCardProps } from './components/metric-card.js';
|
|
11
11
|
import { PipelineStage, PipelineStageMetrics, PipelineStageTiming } from './charts/pipeline-overview.js';
|
|
12
12
|
import { TimelineEvent } from './components/timeline-activity.js';
|
|
13
|
-
import {
|
|
13
|
+
import { ApprovalState } from './components/signal-feedback-inline.js';
|
|
14
14
|
import { LucideIcon } from 'lucide-react';
|
|
15
15
|
|
|
16
16
|
interface TimelineSystemEventsConfig {
|
|
@@ -93,6 +93,8 @@ interface SignalScoreData {
|
|
|
93
93
|
confidence: number;
|
|
94
94
|
urgencyLabel?: SignalScoreUrgencyLabel;
|
|
95
95
|
urgencyExplanation?: string;
|
|
96
|
+
/** Controls whether the priority popover header shows the raw overall score number. @default "number" */
|
|
97
|
+
priorityScoreDisplay?: SignalPriorityScoreDisplay;
|
|
96
98
|
explanationBuckets?: SignalScoreExplanationBucket[];
|
|
97
99
|
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void;
|
|
98
100
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
@@ -181,7 +183,7 @@ interface InboxViewConfig {
|
|
|
181
183
|
}>;
|
|
182
184
|
hideAccountsButton?: boolean;
|
|
183
185
|
accountDetailsLabel?: string;
|
|
184
|
-
onSignalApprove?: (item: QueueItem
|
|
186
|
+
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>;
|
|
185
187
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined;
|
|
186
188
|
signalLabels?: {
|
|
187
189
|
approveButton?: string;
|
|
@@ -412,8 +414,11 @@ interface PriorityFactor {
|
|
|
412
414
|
/** Evidence text (e.g. "$3.4M moved in 8h - current treasury balance $0.00"). */
|
|
413
415
|
rationale: string;
|
|
414
416
|
}
|
|
417
|
+
type SignalPriorityScoreDisplay = "label" | "number";
|
|
415
418
|
interface SignalPriorityPopoverProps {
|
|
416
419
|
score: number;
|
|
420
|
+
/** Controls whether the overall score number is shown in the popover header. @default "number" */
|
|
421
|
+
scoreDisplay?: SignalPriorityScoreDisplay;
|
|
417
422
|
urgencyLabel?: SignalScoreUrgencyLabel;
|
|
418
423
|
/** Synthesis sentence displayed in the popover head. */
|
|
419
424
|
urgencyExplanation?: string;
|
|
@@ -435,6 +440,6 @@ interface SignalPriorityPopoverProps {
|
|
|
435
440
|
/** Persisted priority-level feedback for the footer. */
|
|
436
441
|
initialPriorityFeedback?: PersistedFeedbackData | null;
|
|
437
442
|
}
|
|
438
|
-
declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, initialFactorFeedback, onFactorFeedback, initialPriorityFeedback, }: SignalPriorityPopoverProps): React.JSX.Element;
|
|
443
|
+
declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, initialFactorFeedback, onFactorFeedback, initialPriorityFeedback, scoreDisplay, }: SignalPriorityPopoverProps): React.JSX.Element;
|
|
439
444
|
|
|
440
|
-
export { type AccountFilterTab as A, type BriefStyleVariant as B, type EntityPanelConfig as E, type InboxDetailSections as I, type PriorityFactor as P, type QueueItem as Q, SignalPriorityPopover as S, type TimelineSystemEventsConfig as T, type WorkQueueViewConfig as W, type AccountsViewConfig as a, type AdminTab as b, type AdminViewConfig as c, type EntityPanelSection as d, type InboxSortOption as e, type InboxViewConfig as f, type InsightsCustomTab as g, type InsightsViewConfig as h, type PrototypeBrandConfig as i, type PrototypeConfig as j, type SignalPriorityPopoverProps as k, type SignalScoreData as l, type SignalScoreExplanationBucket as m, type SignalScoreExplanationSignal as n, type SignalScoreUrgencyLabel as o };
|
|
445
|
+
export { type AccountFilterTab as A, type BriefStyleVariant as B, type EntityPanelConfig as E, type InboxDetailSections as I, type PriorityFactor as P, type QueueItem as Q, SignalPriorityPopover as S, type TimelineSystemEventsConfig as T, type WorkQueueViewConfig as W, type AccountsViewConfig as a, type AdminTab as b, type AdminViewConfig as c, type EntityPanelSection as d, type InboxSortOption as e, type InboxViewConfig as f, type InsightsCustomTab as g, type InsightsViewConfig as h, type PrototypeBrandConfig as i, type PrototypeConfig as j, type SignalPriorityPopoverProps as k, type SignalScoreData as l, type SignalScoreExplanationBucket as m, type SignalScoreExplanationSignal as n, type SignalScoreUrgencyLabel as o, type SignalPriorityScoreDisplay as p };
|
package/package.json
CHANGED
|
@@ -149,86 +149,4 @@ describe("CasePanelSignalApprovalActions", () => {
|
|
|
149
149
|
expect(screen.getByText(/this will approve this action for/i)).toBeInTheDocument()
|
|
150
150
|
expect(screen.getByRole("button", { name: /confirm/i }).className).toContain("h-7")
|
|
151
151
|
})
|
|
152
|
-
|
|
153
|
-
it("lets callers collect edited opportunity preview fields before approval", async () => {
|
|
154
|
-
const onApprove = vi.fn()
|
|
155
|
-
|
|
156
|
-
renderApprovalActions({
|
|
157
|
-
initialApprovalState: "confirming",
|
|
158
|
-
onApprove,
|
|
159
|
-
opportunityPreview: {
|
|
160
|
-
name: "Churn Risk - Northwind Systems",
|
|
161
|
-
accountName: "Northwind Systems",
|
|
162
|
-
stage: "Prospecting",
|
|
163
|
-
closeDate: "Jun 30, 2026",
|
|
164
|
-
closeDateValue: "2026-06-30",
|
|
165
|
-
amount: "$75,000",
|
|
166
|
-
amountValue: 75000,
|
|
167
|
-
description: "Initial description",
|
|
168
|
-
churnType: "Churn Risk",
|
|
169
|
-
churnTypeOptions: ["Churn Risk", { value: "Win Back", label: "Win-back" }],
|
|
170
|
-
},
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
fireEvent.change(screen.getByLabelText("Close Date"), { target: { value: "2026-07-15" } })
|
|
174
|
-
expect(screen.getByLabelText("Amount")).toHaveValue("$75,000")
|
|
175
|
-
fireEvent.change(screen.getByLabelText("Amount"), { target: { value: "90000" } })
|
|
176
|
-
fireEvent.change(screen.getByLabelText("Churn Type"), { target: { value: "Win Back" } })
|
|
177
|
-
fireEvent.change(screen.getByLabelText("Description"), { target: { value: "Updated before create" } })
|
|
178
|
-
fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
|
|
179
|
-
|
|
180
|
-
expect(onApprove).toHaveBeenCalledWith({
|
|
181
|
-
closeDate: "2026-07-15",
|
|
182
|
-
amount: "90000",
|
|
183
|
-
churnType: "Win Back",
|
|
184
|
-
description: "Updated before create",
|
|
185
|
-
})
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it("keeps legacy display-only opportunity previews confirmable", () => {
|
|
189
|
-
const onApprove = vi.fn()
|
|
190
|
-
|
|
191
|
-
renderApprovalActions({
|
|
192
|
-
initialApprovalState: "confirming",
|
|
193
|
-
onApprove,
|
|
194
|
-
opportunityPreview: {
|
|
195
|
-
name: "Churn Risk - Northwind Systems",
|
|
196
|
-
accountName: "Northwind Systems",
|
|
197
|
-
stage: "Prospecting",
|
|
198
|
-
closeDate: "Jun 30, 2026",
|
|
199
|
-
amount: "$75,000",
|
|
200
|
-
},
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
expect(screen.getByText(/this will approve this action for/i)).toBeInTheDocument()
|
|
204
|
-
expect(screen.queryByLabelText("Close Date")).not.toBeInTheDocument()
|
|
205
|
-
expect(screen.getByRole("button", { name: /confirm/i })).not.toBeDisabled()
|
|
206
|
-
|
|
207
|
-
fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
|
|
208
|
-
expect(onApprove).toHaveBeenCalledWith(undefined)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it("blocks confirmation until the close date is valid", () => {
|
|
212
|
-
const onApprove = vi.fn()
|
|
213
|
-
|
|
214
|
-
renderApprovalActions({
|
|
215
|
-
initialApprovalState: "confirming",
|
|
216
|
-
onApprove,
|
|
217
|
-
opportunityPreview: {
|
|
218
|
-
name: "Churn Risk - Northwind Systems",
|
|
219
|
-
accountName: "Northwind Systems",
|
|
220
|
-
stage: "Prospecting",
|
|
221
|
-
closeDate: "Jun 30, 2026",
|
|
222
|
-
closeDateValue: "2026-06-30",
|
|
223
|
-
amount: "$75,000",
|
|
224
|
-
},
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
fireEvent.change(screen.getByLabelText("Close Date"), { target: { value: "" } })
|
|
228
|
-
expect(screen.getByRole("button", { name: /confirm/i })).toBeDisabled()
|
|
229
|
-
expect(screen.getByText("Enter a valid close date.")).toBeInTheDocument()
|
|
230
|
-
|
|
231
|
-
fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
|
|
232
|
-
expect(onApprove).not.toHaveBeenCalled()
|
|
233
|
-
})
|
|
234
152
|
})
|
|
@@ -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,36 @@ 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 calibrated priority copy", () => {
|
|
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 = weighted signals + calibration")
|
|
198
|
+
expect(content.textContent).not.toContain("Score = weighted sum")
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
it("renders the overall score number by default while preserving factor row scores", () => {
|
|
203
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
204
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
205
|
+
|
|
206
|
+
expect(screen.getByTestId("priority-overall-score").textContent).toBe("79/100")
|
|
207
|
+
expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
|
|
208
|
+
expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("hides only the overall header score in label display mode", () => {
|
|
212
|
+
render(<SignalPriorityPopover {...defaultProps} scoreDisplay="label" />)
|
|
213
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
214
|
+
|
|
215
|
+
const header = screen.getByTestId("priority-popover-header")
|
|
216
|
+
expect(screen.queryByTestId("priority-overall-score")).toBeNull()
|
|
217
|
+
expect(header.textContent).toContain("Why this is high priority")
|
|
218
|
+
expect(header.textContent).not.toContain("79/100")
|
|
219
|
+
expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
|
|
220
|
+
expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
|
|
198
221
|
})
|
|
199
222
|
|
|
200
223
|
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
|
}
|
|
@@ -225,8 +205,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
225
205
|
setApprovalState("pending")
|
|
226
206
|
}, [])
|
|
227
207
|
|
|
228
|
-
const approve = React.useCallback((
|
|
229
|
-
const result = onApprove?.(
|
|
208
|
+
const approve = React.useCallback(() => {
|
|
209
|
+
const result = onApprove?.()
|
|
230
210
|
// If the callback returns a Promise, show a loading state and wait for it.
|
|
231
211
|
if (result && typeof (result as Promise<boolean>).then === "function") {
|
|
232
212
|
setApprovalState("creating")
|
|
@@ -437,49 +417,6 @@ function SubmittedFeedback({
|
|
|
437
417
|
)
|
|
438
418
|
}
|
|
439
419
|
|
|
440
|
-
function optionValue(option: string | OpportunityPreviewOption): string {
|
|
441
|
-
return typeof option === "string" ? option : option.value
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function optionLabel(option: string | OpportunityPreviewOption): string {
|
|
445
|
-
return typeof option === "string" ? option : option.label
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function formatAmountDraftValue(value: string | number | null | undefined): string {
|
|
449
|
-
if (value == null || value === "") return ""
|
|
450
|
-
if (typeof value === "number") {
|
|
451
|
-
return new Intl.NumberFormat("en-US", {
|
|
452
|
-
style: "currency",
|
|
453
|
-
currency: "USD",
|
|
454
|
-
maximumFractionDigits: 0,
|
|
455
|
-
}).format(value)
|
|
456
|
-
}
|
|
457
|
-
return value
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
|
|
461
|
-
return {
|
|
462
|
-
closeDate: preview?.closeDateValue ?? preview?.closeDate ?? "",
|
|
463
|
-
amount: preview?.amountValue === undefined
|
|
464
|
-
? preview?.amount ?? ""
|
|
465
|
-
: formatAmountDraftValue(preview.amountValue),
|
|
466
|
-
description: preview?.description ?? "",
|
|
467
|
-
churnType: preview?.churnType ?? "",
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function hasEditableOpportunityPreview(preview?: OpportunityPreview): boolean {
|
|
472
|
-
return !!preview && isValidDateInput(preview.closeDateValue ?? preview.closeDate)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function isValidDateInput(value: string): boolean {
|
|
476
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
|
477
|
-
if (!match) return false
|
|
478
|
-
const parsed = new Date(`${value}T00:00:00Z`)
|
|
479
|
-
if (Number.isNaN(parsed.getTime())) return false
|
|
480
|
-
return parsed.toISOString().slice(0, 10) === value
|
|
481
|
-
}
|
|
482
|
-
|
|
483
420
|
function Actions() {
|
|
484
421
|
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
485
422
|
useSignalApproval()
|
|
@@ -489,16 +426,6 @@ function Actions() {
|
|
|
489
426
|
const [detailText, setDetailText] = React.useState("")
|
|
490
427
|
const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
|
|
491
428
|
const [isEditing, setIsEditing] = React.useState(false)
|
|
492
|
-
const [opportunityDraft, setOpportunityDraft] = React.useState<OpportunityDraft>(() => buildOpportunityDraft(opportunityPreview))
|
|
493
|
-
|
|
494
|
-
React.useEffect(() => {
|
|
495
|
-
if (approvalState === "confirming") {
|
|
496
|
-
setOpportunityDraft(buildOpportunityDraft(opportunityPreview))
|
|
497
|
-
}
|
|
498
|
-
}, [approvalState, opportunityPreview])
|
|
499
|
-
|
|
500
|
-
const churnTypeOptions = opportunityPreview?.churnTypeOptions ?? []
|
|
501
|
-
const hasChurnTypeOptions = churnTypeOptions.length > 0
|
|
502
429
|
|
|
503
430
|
const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
|
|
504
431
|
const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
|
|
@@ -511,8 +438,6 @@ function Actions() {
|
|
|
511
438
|
(!needsText || detailText.trim().length > 0)
|
|
512
439
|
|
|
513
440
|
const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
|
|
514
|
-
const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
|
|
515
|
-
const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
|
|
516
441
|
|
|
517
442
|
const selectTopReason = (label: string) => {
|
|
518
443
|
if (selectedTopReason === label) {
|
|
@@ -814,8 +739,8 @@ function Actions() {
|
|
|
814
739
|
<p className="text-sm text-foreground">
|
|
815
740
|
{labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
|
|
816
741
|
</p>
|
|
817
|
-
{opportunityPreview &&
|
|
818
|
-
<div className="mt-3 space-y-
|
|
742
|
+
{opportunityPreview && (
|
|
743
|
+
<div className="mt-3 space-y-1.5 border-t border-border/50 pt-3">
|
|
819
744
|
{[
|
|
820
745
|
{ label: "Opportunity", value: opportunityPreview.name },
|
|
821
746
|
{ label: "Account", value: opportunityPreview.accountName },
|
|
@@ -823,105 +748,19 @@ function Actions() {
|
|
|
823
748
|
{ label: "Close Date", value: opportunityPreview.closeDate },
|
|
824
749
|
{ label: "Amount", value: opportunityPreview.amount },
|
|
825
750
|
].map(({ label, value }) => (
|
|
826
|
-
<div key={label} className="flex items-center justify-between
|
|
827
|
-
<span className="text-muted-foreground">{label}</span>
|
|
828
|
-
<span className="text-right font-medium text-foreground">{value}</span>
|
|
829
|
-
</div>
|
|
830
|
-
))}
|
|
831
|
-
</div>
|
|
832
|
-
)}
|
|
833
|
-
{opportunityPreview && isEditableOpportunityPreview && (
|
|
834
|
-
<div className="mt-3 space-y-3 border-t border-border/50 pt-3">
|
|
835
|
-
{[
|
|
836
|
-
{ label: "Opportunity", value: opportunityPreview.name },
|
|
837
|
-
{ label: "Account", value: opportunityPreview.accountName },
|
|
838
|
-
{ label: "Stage", value: opportunityPreview.stage },
|
|
839
|
-
].map(({ label, value }) => (
|
|
840
|
-
<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">
|
|
841
752
|
<span className="text-muted-foreground">{label}</span>
|
|
842
|
-
<span className="
|
|
753
|
+
<span className="font-medium text-foreground">{value}</span>
|
|
843
754
|
</div>
|
|
844
755
|
))}
|
|
845
|
-
|
|
846
|
-
<div className="grid gap-2 sm:grid-cols-2">
|
|
847
|
-
<label className="space-y-1 text-xs">
|
|
848
|
-
<span className="font-medium text-muted-foreground">Close Date</span>
|
|
849
|
-
<input
|
|
850
|
-
type="date"
|
|
851
|
-
value={opportunityDraft.closeDate}
|
|
852
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, closeDate: event.target.value }))}
|
|
853
|
-
aria-invalid={!canConfirmOpportunity}
|
|
854
|
-
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"
|
|
855
|
-
/>
|
|
856
|
-
{!canConfirmOpportunity && (
|
|
857
|
-
<span className="text-[11px] text-red-600">Enter a valid close date.</span>
|
|
858
|
-
)}
|
|
859
|
-
</label>
|
|
860
|
-
|
|
861
|
-
<label className="space-y-1 text-xs">
|
|
862
|
-
<span className="font-medium text-muted-foreground">Amount</span>
|
|
863
|
-
<input
|
|
864
|
-
type="text"
|
|
865
|
-
inputMode="decimal"
|
|
866
|
-
value={opportunityDraft.amount}
|
|
867
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, amount: event.target.value }))}
|
|
868
|
-
placeholder={opportunityPreview.amount}
|
|
869
|
-
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"
|
|
870
|
-
/>
|
|
871
|
-
</label>
|
|
872
|
-
</div>
|
|
873
|
-
|
|
874
|
-
<label className="space-y-1 text-xs">
|
|
875
|
-
<span className="font-medium text-muted-foreground">Churn Type</span>
|
|
876
|
-
{hasChurnTypeOptions ? (
|
|
877
|
-
<select
|
|
878
|
-
value={opportunityDraft.churnType}
|
|
879
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
|
|
880
|
-
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"
|
|
881
|
-
>
|
|
882
|
-
<option value="">No churn type</option>
|
|
883
|
-
{churnTypeOptions.map((option) => (
|
|
884
|
-
<option key={optionValue(option)} value={optionValue(option)}>
|
|
885
|
-
{optionLabel(option)}
|
|
886
|
-
</option>
|
|
887
|
-
))}
|
|
888
|
-
</select>
|
|
889
|
-
) : (
|
|
890
|
-
<input
|
|
891
|
-
type="text"
|
|
892
|
-
value={opportunityDraft.churnType}
|
|
893
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
|
|
894
|
-
placeholder="No churn type"
|
|
895
|
-
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"
|
|
896
|
-
/>
|
|
897
|
-
)}
|
|
898
|
-
</label>
|
|
899
|
-
|
|
900
|
-
<label className="space-y-1 text-xs">
|
|
901
|
-
<span className="font-medium text-muted-foreground">Description</span>
|
|
902
|
-
<textarea
|
|
903
|
-
value={opportunityDraft.description}
|
|
904
|
-
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, description: event.target.value }))}
|
|
905
|
-
rows={3}
|
|
906
|
-
placeholder="Add a short description"
|
|
907
|
-
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"
|
|
908
|
-
/>
|
|
909
|
-
</label>
|
|
910
756
|
</div>
|
|
911
757
|
)}
|
|
912
758
|
</div>
|
|
913
759
|
<div className="flex items-center gap-2">
|
|
914
760
|
<button
|
|
915
761
|
type="button"
|
|
916
|
-
onClick={
|
|
917
|
-
|
|
918
|
-
approve()
|
|
919
|
-
return
|
|
920
|
-
}
|
|
921
|
-
approve(isEditableOpportunityPreview ? opportunityDraft : undefined)
|
|
922
|
-
}}
|
|
923
|
-
disabled={!canConfirmOpportunity}
|
|
924
|
-
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"
|
|
925
764
|
>
|
|
926
765
|
<Check className="h-3 w-3" />
|
|
927
766
|
Confirm
|
|
@@ -1022,4 +861,5 @@ export {
|
|
|
1022
861
|
Gate as SignalApprovalGate,
|
|
1023
862
|
}
|
|
1024
863
|
export const SignalApproval = { Root, Actions, Gate }
|
|
1025
|
-
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}
|