@handled-ai/design-system 0.18.45 → 0.18.47
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 +6 -21
- package/dist/components/data-table-filter.js +9 -134
- 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 +139 -10
- package/dist/components/signal-feedback-inline.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- 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 -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-B5b-XZ7i.d.ts → signal-priority-popover-QJngMAj7.d.ts} +2 -2
- package/package.json +1 -1
- package/src/components/__tests__/case-panel-why.test.tsx +82 -0
- package/src/components/__tests__/data-table-filter.test.tsx +0 -130
- package/src/components/data-table-filter.tsx +9 -160
- package/src/components/signal-feedback-inline.tsx +174 -20
- package/src/prototype/prototype-config.ts +2 -2
- package/src/prototype/prototype-inbox-view.tsx +3 -3
|
@@ -28,10 +28,13 @@ export interface FilterOption {
|
|
|
28
28
|
value: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
interface
|
|
31
|
+
export interface DataTableFilterCategory {
|
|
32
32
|
id: string
|
|
33
33
|
label: string
|
|
34
34
|
icon: React.ComponentType<{ className?: string }>
|
|
35
|
+
options: (string | FilterOption)[]
|
|
36
|
+
/** Filter behavior. Defaults to "multi" (checkbox multi-select). */
|
|
37
|
+
type?: "multi" | "single" | "boolean"
|
|
35
38
|
/**
|
|
36
39
|
* Submenu search behavior. Defaults to the DataTableFilter
|
|
37
40
|
* optionSearchThreshold prop. Use true to always show search or false to
|
|
@@ -40,25 +43,6 @@ interface DataTableFilterCategoryBase {
|
|
|
40
43
|
searchable?: boolean | { threshold?: number }
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export interface DataTableOptionFilterCategory extends DataTableFilterCategoryBase {
|
|
44
|
-
options: (string | FilterOption)[]
|
|
45
|
-
/** Filter behavior. Defaults to "multi" (checkbox multi-select). */
|
|
46
|
-
type?: "multi" | "single" | "boolean"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DataTableTextFilterCategory extends DataTableFilterCategoryBase {
|
|
50
|
-
/** Free-text filter behavior. Renders a top-level submenu with a text input. */
|
|
51
|
-
type: "text"
|
|
52
|
-
/** Placeholder shown in the text filter input. */
|
|
53
|
-
valuePlaceholder?: string
|
|
54
|
-
/** Not used for text filters; optional for backwards-compatible category shapes. */
|
|
55
|
-
options?: (string | FilterOption)[]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type DataTableFilterCategory =
|
|
59
|
-
| DataTableOptionFilterCategory
|
|
60
|
-
| DataTableTextFilterCategory
|
|
61
|
-
|
|
62
46
|
function getOptionValue(option: string | FilterOption): string {
|
|
63
47
|
return typeof option === "string" ? option : option.value
|
|
64
48
|
}
|
|
@@ -66,111 +50,6 @@ function getOptionLabel(option: string | FilterOption): string {
|
|
|
66
50
|
return typeof option === "string" ? option : option.label
|
|
67
51
|
}
|
|
68
52
|
|
|
69
|
-
function isTextFilterCategory(
|
|
70
|
-
category: DataTableFilterCategory
|
|
71
|
-
): category is DataTableTextFilterCategory {
|
|
72
|
-
return category.type === "text"
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function TextFilterSubmenu({
|
|
76
|
-
category,
|
|
77
|
-
value,
|
|
78
|
-
onValueChange,
|
|
79
|
-
}: {
|
|
80
|
-
category: DataTableTextFilterCategory
|
|
81
|
-
value: string
|
|
82
|
-
onValueChange?: (categoryId: string, value: string) => void
|
|
83
|
-
}) {
|
|
84
|
-
const [draftValue, setDraftValue] = React.useState(value)
|
|
85
|
-
|
|
86
|
-
React.useEffect(() => {
|
|
87
|
-
setDraftValue(value)
|
|
88
|
-
}, [value])
|
|
89
|
-
|
|
90
|
-
const active = value.trim().length > 0
|
|
91
|
-
const applyValue = React.useCallback(() => {
|
|
92
|
-
onValueChange?.(category.id, draftValue.trim())
|
|
93
|
-
}, [category.id, draftValue, onValueChange])
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<DropdownMenuSub
|
|
97
|
-
onOpenChange={(open) => {
|
|
98
|
-
if (!open) {
|
|
99
|
-
setDraftValue(value)
|
|
100
|
-
}
|
|
101
|
-
}}
|
|
102
|
-
>
|
|
103
|
-
<DropdownMenuSubTrigger
|
|
104
|
-
className={cn(
|
|
105
|
-
"cursor-pointer py-1.5 text-xs",
|
|
106
|
-
active && "text-brand-purple"
|
|
107
|
-
)}
|
|
108
|
-
>
|
|
109
|
-
<category.icon
|
|
110
|
-
className={cn(
|
|
111
|
-
"mr-2 h-3.5 w-3.5 text-muted-foreground",
|
|
112
|
-
active && "text-brand-purple"
|
|
113
|
-
)}
|
|
114
|
-
/>
|
|
115
|
-
{category.label}
|
|
116
|
-
{active ? <Check className="ml-auto h-4 w-4" /> : null}
|
|
117
|
-
</DropdownMenuSubTrigger>
|
|
118
|
-
<DropdownMenuSubContent className="w-64 p-2">
|
|
119
|
-
<div className="space-y-2">
|
|
120
|
-
<input
|
|
121
|
-
aria-label={category.label}
|
|
122
|
-
className="h-8 w-full rounded-md bg-muted/50 px-2 py-1 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted"
|
|
123
|
-
placeholder={
|
|
124
|
-
category.valuePlaceholder ??
|
|
125
|
-
`Enter ${category.label.toLowerCase()}...`
|
|
126
|
-
}
|
|
127
|
-
value={draftValue}
|
|
128
|
-
onChange={(event) => setDraftValue(event.target.value)}
|
|
129
|
-
onClick={(event) => event.stopPropagation()}
|
|
130
|
-
onKeyDown={(event) => {
|
|
131
|
-
event.stopPropagation()
|
|
132
|
-
if (event.key === "Enter") {
|
|
133
|
-
event.preventDefault()
|
|
134
|
-
applyValue()
|
|
135
|
-
}
|
|
136
|
-
}}
|
|
137
|
-
/>
|
|
138
|
-
<div className="flex items-center justify-end gap-2">
|
|
139
|
-
{active ? (
|
|
140
|
-
<Button
|
|
141
|
-
type="button"
|
|
142
|
-
variant="ghost"
|
|
143
|
-
size="sm"
|
|
144
|
-
className="h-7 px-2 text-xs"
|
|
145
|
-
onClick={(event) => {
|
|
146
|
-
event.preventDefault()
|
|
147
|
-
event.stopPropagation()
|
|
148
|
-
setDraftValue("")
|
|
149
|
-
onValueChange?.(category.id, "")
|
|
150
|
-
}}
|
|
151
|
-
>
|
|
152
|
-
Clear
|
|
153
|
-
</Button>
|
|
154
|
-
) : null}
|
|
155
|
-
<Button
|
|
156
|
-
type="button"
|
|
157
|
-
size="sm"
|
|
158
|
-
className="h-7 px-2 text-xs"
|
|
159
|
-
onClick={(event) => {
|
|
160
|
-
event.preventDefault()
|
|
161
|
-
event.stopPropagation()
|
|
162
|
-
applyValue()
|
|
163
|
-
}}
|
|
164
|
-
>
|
|
165
|
-
Apply
|
|
166
|
-
</Button>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</DropdownMenuSubContent>
|
|
170
|
-
</DropdownMenuSub>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
53
|
export interface DataTableFilterProps {
|
|
175
54
|
categories: DataTableFilterCategory[]
|
|
176
55
|
selectedFilters: Record<string, string[]>
|
|
@@ -192,10 +71,6 @@ export interface DataTableFilterProps {
|
|
|
192
71
|
onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void
|
|
193
72
|
/** Dropdown entry label for the condition-builder panel. Default: "Add filter". */
|
|
194
73
|
conditionBuilderLabel?: string
|
|
195
|
-
/** Active free-text filters keyed by category id. */
|
|
196
|
-
textFilters?: Record<string, string>
|
|
197
|
-
/** Callback when a free-text filter value is applied or cleared. */
|
|
198
|
-
onTextFilterChange?: (categoryId: string, value: string) => void
|
|
199
74
|
}
|
|
200
75
|
|
|
201
76
|
export function DataTableFilter({
|
|
@@ -211,8 +86,6 @@ export function DataTableFilter({
|
|
|
211
86
|
conditionFilters = [],
|
|
212
87
|
onConditionFiltersChange,
|
|
213
88
|
conditionBuilderLabel = "Add filter",
|
|
214
|
-
textFilters = {},
|
|
215
|
-
onTextFilterChange,
|
|
216
89
|
}: DataTableFilterProps) {
|
|
217
90
|
const [query, setQuery] = React.useState("")
|
|
218
91
|
const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})
|
|
@@ -230,10 +103,6 @@ export function DataTableFilter({
|
|
|
230
103
|
return true
|
|
231
104
|
}
|
|
232
105
|
|
|
233
|
-
if (isTextFilterCategory(category)) {
|
|
234
|
-
return false
|
|
235
|
-
}
|
|
236
|
-
|
|
237
106
|
return category.options.some((option) =>
|
|
238
107
|
getOptionLabel(option).toLowerCase().includes(normalized)
|
|
239
108
|
)
|
|
@@ -254,16 +123,8 @@ export function DataTableFilter({
|
|
|
254
123
|
0
|
|
255
124
|
)
|
|
256
125
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return count
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return textFilters[category.id]?.trim() ? count + 1 : count
|
|
263
|
-
}, 0)
|
|
264
|
-
|
|
265
|
-
return userCount + conditionFilters.length + textCount
|
|
266
|
-
}, [categories, selectedFilters, conditionFilters.length, textFilters])
|
|
126
|
+
return userCount + conditionFilters.length
|
|
127
|
+
}, [selectedFilters, conditionFilters.length])
|
|
267
128
|
|
|
268
129
|
/** Collect all preset chips to render */
|
|
269
130
|
const presetChips = React.useMemo(() => {
|
|
@@ -274,9 +135,9 @@ export function DataTableFilter({
|
|
|
274
135
|
for (const [categoryId, values] of Object.entries(presetFilters)) {
|
|
275
136
|
const category = categories.find((c) => c.id === categoryId)
|
|
276
137
|
for (const value of values) {
|
|
277
|
-
const option = category
|
|
278
|
-
|
|
279
|
-
|
|
138
|
+
const option = category?.options.find(
|
|
139
|
+
(opt) => getOptionValue(opt) === value
|
|
140
|
+
)
|
|
280
141
|
const label = option ? getOptionLabel(option) : value
|
|
281
142
|
const active = selectedFilters[categoryId]?.includes(value) ?? false
|
|
282
143
|
chips.push({ categoryId, value, label, active })
|
|
@@ -347,18 +208,6 @@ export function DataTableFilter({
|
|
|
347
208
|
)
|
|
348
209
|
}
|
|
349
210
|
|
|
350
|
-
/* ── Free-text submenu ───────────────────────────────── */
|
|
351
|
-
if (isTextFilterCategory(category)) {
|
|
352
|
-
return (
|
|
353
|
-
<TextFilterSubmenu
|
|
354
|
-
key={category.id}
|
|
355
|
-
category={category}
|
|
356
|
-
value={textFilters[category.id] ?? ""}
|
|
357
|
-
onValueChange={onTextFilterChange}
|
|
358
|
-
/>
|
|
359
|
-
)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
211
|
/* ── Sub-menu (single / multi) ──────────────────────── */
|
|
363
212
|
const subQuery = (subQueries[category.id] ?? "").trim().toLowerCase()
|
|
364
213
|
const filteredOptions = subQuery
|
|
@@ -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
|
}
|
|
@@ -205,8 +225,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
205
225
|
setApprovalState("pending")
|
|
206
226
|
}, [])
|
|
207
227
|
|
|
208
|
-
const approve = React.useCallback(() => {
|
|
209
|
-
const result = onApprove?.()
|
|
228
|
+
const approve = React.useCallback((draft?: OpportunityDraft) => {
|
|
229
|
+
const result = onApprove?.(draft)
|
|
210
230
|
// If the callback returns a Promise, show a loading state and wait for it.
|
|
211
231
|
if (result && typeof (result as Promise<boolean>).then === "function") {
|
|
212
232
|
setApprovalState("creating")
|
|
@@ -417,6 +437,49 @@ function SubmittedFeedback({
|
|
|
417
437
|
)
|
|
418
438
|
}
|
|
419
439
|
|
|
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
|
+
|
|
420
483
|
function Actions() {
|
|
421
484
|
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
422
485
|
useSignalApproval()
|
|
@@ -426,6 +489,16 @@ function Actions() {
|
|
|
426
489
|
const [detailText, setDetailText] = React.useState("")
|
|
427
490
|
const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
|
|
428
491
|
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
|
|
429
502
|
|
|
430
503
|
const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
|
|
431
504
|
const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
|
|
@@ -438,6 +511,8 @@ function Actions() {
|
|
|
438
511
|
(!needsText || detailText.trim().length > 0)
|
|
439
512
|
|
|
440
513
|
const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
|
|
514
|
+
const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
|
|
515
|
+
const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
|
|
441
516
|
|
|
442
517
|
const selectTopReason = (label: string) => {
|
|
443
518
|
if (selectedTopReason === label) {
|
|
@@ -739,8 +814,8 @@ function Actions() {
|
|
|
739
814
|
<p className="text-sm text-foreground">
|
|
740
815
|
{labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
|
|
741
816
|
</p>
|
|
742
|
-
{opportunityPreview && (
|
|
743
|
-
<div className="mt-3 space-y-
|
|
817
|
+
{opportunityPreview && !isEditableOpportunityPreview && (
|
|
818
|
+
<div className="mt-3 space-y-2 border-t border-border/50 pt-3">
|
|
744
819
|
{[
|
|
745
820
|
{ label: "Opportunity", value: opportunityPreview.name },
|
|
746
821
|
{ label: "Account", value: opportunityPreview.accountName },
|
|
@@ -748,19 +823,99 @@ function Actions() {
|
|
|
748
823
|
{ label: "Close Date", value: opportunityPreview.closeDate },
|
|
749
824
|
{ label: "Amount", value: opportunityPreview.amount },
|
|
750
825
|
].map(({ label, value }) => (
|
|
751
|
-
<div key={label} className="flex items-center justify-between text-xs">
|
|
826
|
+
<div key={label} className="flex items-center justify-between gap-3 text-xs">
|
|
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">
|
|
752
841
|
<span className="text-muted-foreground">{label}</span>
|
|
753
|
-
<span className="font-medium text-foreground">{value}</span>
|
|
842
|
+
<span className="text-right font-medium text-foreground">{value}</span>
|
|
754
843
|
</div>
|
|
755
844
|
))}
|
|
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>
|
|
756
910
|
</div>
|
|
757
911
|
)}
|
|
758
912
|
</div>
|
|
759
913
|
<div className="flex items-center gap-2">
|
|
760
914
|
<button
|
|
761
915
|
type="button"
|
|
762
|
-
onClick={approve}
|
|
763
|
-
|
|
916
|
+
onClick={() => approve(isEditableOpportunityPreview ? opportunityDraft : undefined)}
|
|
917
|
+
disabled={!canConfirmOpportunity}
|
|
918
|
+
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
919
|
>
|
|
765
920
|
<Check className="h-3 w-3" />
|
|
766
921
|
Confirm
|
|
@@ -861,5 +1016,4 @@ export {
|
|
|
861
1016
|
Gate as SignalApprovalGate,
|
|
862
1017
|
}
|
|
863
1018
|
export const SignalApproval = { Root, Actions, Gate }
|
|
864
|
-
export type OpportunityPreview
|
|
865
|
-
export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
1019
|
+
export type { ApprovalState, OpportunityPreview, OpportunityDraft, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
@@ -13,7 +13,7 @@ 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
18
|
import type { PriorityFactor } from "../components/signal-priority-popover"
|
|
19
19
|
import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
|
|
@@ -180,7 +180,7 @@ export interface InboxViewConfig {
|
|
|
180
180
|
quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
|
|
181
181
|
hideAccountsButton?: boolean
|
|
182
182
|
accountDetailsLabel?: string
|
|
183
|
-
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
183
|
+
onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
|
|
184
184
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
185
185
|
signalLabels?: {
|
|
186
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 })
|