@wealthx/shadcn 1.5.1 → 1.5.3
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/.turbo/turbo-build.log +115 -115
- package/CHANGELOG.md +12 -0
- package/dist/chunk-G2EWIP2N.mjs +960 -0
- package/dist/{chunk-MHHA7QGO.mjs → chunk-ODO6BUOF.mjs} +1 -1
- package/dist/chunk-PX4M67XQ.mjs +301 -0
- package/dist/{chunk-FYUSF5KO.mjs → chunk-QRVEI6J3.mjs} +1 -1
- package/dist/{chunk-42NEC57Y.mjs → chunk-RAKBWNQH.mjs} +272 -3
- package/dist/components/ui/{contact-alert-dialog.js → contact-alert-dialog/index.js} +1029 -593
- package/dist/components/ui/contact-alert-dialog/index.mjs +31 -0
- package/dist/components/ui/file-preview-dialog.js +407 -100
- package/dist/components/ui/file-preview-dialog.mjs +3 -1
- package/dist/components/ui/kanban-column.js +408 -113
- package/dist/components/ui/kanban-column.mjs +3 -2
- package/dist/components/ui/opportunity-card.js +383 -88
- package/dist/components/ui/opportunity-card.mjs +2 -1
- package/dist/components/ui/pipeline-board.js +424 -129
- package/dist/components/ui/pipeline-board.mjs +4 -3
- package/dist/index.js +3081 -2282
- package/dist/index.mjs +39 -35
- package/dist/styles.css +1 -1
- package/package.json +5 -4
- package/src/components/index.tsx +3 -2
- package/src/components/ui/contact-alert-dialog/builder-ui.tsx +556 -0
- package/src/components/ui/contact-alert-dialog/config.ts +262 -0
- package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +214 -0
- package/src/components/ui/contact-alert-dialog/index.tsx +15 -0
- package/src/components/ui/contact-alert-dialog/types.ts +61 -0
- package/src/components/ui/contact-alert-dialog/utils.ts +93 -0
- package/src/components/ui/file-preview-dialog.tsx +299 -99
- package/src/components/ui/opportunity-card.tsx +328 -1
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +1 -1
- package/dist/chunk-5WMFKQZ6.mjs +0 -180
- package/dist/chunk-Y24TXIFJ.mjs +0 -518
- package/dist/components/ui/contact-alert-dialog.mjs +0 -27
- package/src/components/ui/contact-alert-dialog.tsx +0 -710
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { PlusIcon, Trash2Icon } from "lucide-react";
|
|
3
|
-
import {
|
|
4
|
-
Dialog,
|
|
5
|
-
DialogContent,
|
|
6
|
-
DialogHeader,
|
|
7
|
-
DialogTitle,
|
|
8
|
-
DialogFooter,
|
|
9
|
-
} from "@/components/ui/dialog";
|
|
10
|
-
import { Button } from "@/components/ui/button";
|
|
11
|
-
import { Input } from "@/components/ui/input";
|
|
12
|
-
import {
|
|
13
|
-
InputGroup,
|
|
14
|
-
InputGroupAddon,
|
|
15
|
-
InputGroupInput,
|
|
16
|
-
InputGroupText,
|
|
17
|
-
} from "@/components/ui/input-group";
|
|
18
|
-
import { Checkbox } from "@/components/ui/checkbox";
|
|
19
|
-
import { Label } from "@/components/ui/label";
|
|
20
|
-
import { Field, FieldLabel } from "@/components/ui/field";
|
|
21
|
-
import {
|
|
22
|
-
Select,
|
|
23
|
-
SelectContent,
|
|
24
|
-
SelectItem,
|
|
25
|
-
SelectTrigger,
|
|
26
|
-
SelectValue,
|
|
27
|
-
} from "@/components/ui/select";
|
|
28
|
-
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
29
|
-
import { cn } from "@/lib/utils";
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Types
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
export type AlertQueryCombinator = "AND" | "OR";
|
|
36
|
-
|
|
37
|
-
export type AlertQueryOperator =
|
|
38
|
-
| "equal"
|
|
39
|
-
| "not_equal"
|
|
40
|
-
| "less"
|
|
41
|
-
| "less_or_equal"
|
|
42
|
-
| "greater"
|
|
43
|
-
| "greater_or_equal"
|
|
44
|
-
| "between";
|
|
45
|
-
|
|
46
|
-
export type AlertQueryFieldType = "number" | "boolean";
|
|
47
|
-
|
|
48
|
-
export type ContactAlertSeverity = "INSIGHT" | "WATCH" | "NEED_ACTION";
|
|
49
|
-
|
|
50
|
-
export interface AlertQueryField {
|
|
51
|
-
key: string;
|
|
52
|
-
label: string;
|
|
53
|
-
type: AlertQueryFieldType;
|
|
54
|
-
/** "dollar" = prefix "$", "percent" = suffix "%" */
|
|
55
|
-
unit?: "dollar" | "percent";
|
|
56
|
-
/** Allowed operators (defaults to all numeric operators). */
|
|
57
|
-
operators?: AlertQueryOperator[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface AlertQueryRule {
|
|
61
|
-
id: string;
|
|
62
|
-
field: string;
|
|
63
|
-
operator: AlertQueryOperator;
|
|
64
|
-
value: string;
|
|
65
|
-
/** Second value used by the "between" operator. */
|
|
66
|
-
value2?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface AlertQueryGroup {
|
|
70
|
-
rules: AlertQueryRule[];
|
|
71
|
-
/**
|
|
72
|
-
* Combinator connecting rule[i] to rule[i+1].
|
|
73
|
-
* Length is always rules.length - 1.
|
|
74
|
-
*/
|
|
75
|
-
logicalOps: AlertQueryCombinator[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface ContactAlertQueryBuilderProps {
|
|
79
|
-
value: AlertQueryGroup;
|
|
80
|
-
onChange: (value: AlertQueryGroup) => void;
|
|
81
|
-
fields: AlertQueryField[];
|
|
82
|
-
className?: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface ContactAlertDialogProps {
|
|
86
|
-
open: boolean;
|
|
87
|
-
onOpenChange: (open: boolean) => void;
|
|
88
|
-
mode?: "create" | "edit";
|
|
89
|
-
initialName?: string;
|
|
90
|
-
initialSeverity?: ContactAlertSeverity;
|
|
91
|
-
initialQuery?: AlertQueryGroup;
|
|
92
|
-
/** Show "Share across company" checkbox for company admins. */
|
|
93
|
-
isCompanyAdmin?: boolean;
|
|
94
|
-
initialShareAcrossCompany?: boolean;
|
|
95
|
-
onSave: (data: {
|
|
96
|
-
name: string;
|
|
97
|
-
severity: ContactAlertSeverity;
|
|
98
|
-
query: AlertQueryGroup;
|
|
99
|
-
shareAcrossCompany: boolean;
|
|
100
|
-
}) => void;
|
|
101
|
-
isLoading?: boolean;
|
|
102
|
-
className?: string;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Constants
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
export const ALERT_QUERY_FIELDS: AlertQueryField[] = [
|
|
110
|
-
{
|
|
111
|
-
key: "userMetric.max_loan_amount",
|
|
112
|
-
label: "Borrowing Capacity",
|
|
113
|
-
type: "number",
|
|
114
|
-
unit: "dollar",
|
|
115
|
-
operators: [
|
|
116
|
-
"equal",
|
|
117
|
-
"less",
|
|
118
|
-
"less_or_equal",
|
|
119
|
-
"greater",
|
|
120
|
-
"greater_or_equal",
|
|
121
|
-
"between",
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
key: "userMetric.debt_outstanding",
|
|
126
|
-
label: "Outstanding Debt",
|
|
127
|
-
type: "number",
|
|
128
|
-
unit: "dollar",
|
|
129
|
-
operators: [
|
|
130
|
-
"equal",
|
|
131
|
-
"less",
|
|
132
|
-
"less_or_equal",
|
|
133
|
-
"greater",
|
|
134
|
-
"greater_or_equal",
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
key: "userMetric.lvr",
|
|
139
|
-
label: "Current LVR",
|
|
140
|
-
type: "number",
|
|
141
|
-
unit: "percent",
|
|
142
|
-
operators: [
|
|
143
|
-
"equal",
|
|
144
|
-
"less",
|
|
145
|
-
"less_or_equal",
|
|
146
|
-
"greater",
|
|
147
|
-
"greater_or_equal",
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
key: "userMetric.has_met_buying_goal",
|
|
152
|
-
label: "Has Met Buying Goal",
|
|
153
|
-
type: "boolean",
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
key: "userMetric.excess_monthly_surplus",
|
|
157
|
-
label: "Excess Monthly Surplus",
|
|
158
|
-
type: "number",
|
|
159
|
-
unit: "dollar",
|
|
160
|
-
operators: [
|
|
161
|
-
"equal",
|
|
162
|
-
"less",
|
|
163
|
-
"less_or_equal",
|
|
164
|
-
"greater",
|
|
165
|
-
"greater_or_equal",
|
|
166
|
-
],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
key: "userMetric.equity",
|
|
170
|
-
label: "Equity Amount",
|
|
171
|
-
type: "number",
|
|
172
|
-
unit: "dollar",
|
|
173
|
-
operators: [
|
|
174
|
-
"equal",
|
|
175
|
-
"less",
|
|
176
|
-
"less_or_equal",
|
|
177
|
-
"greater",
|
|
178
|
-
"greater_or_equal",
|
|
179
|
-
],
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
key: "userMetric.max_debt_interest_rate",
|
|
183
|
-
label: "Max Debt Interest Rate",
|
|
184
|
-
type: "number",
|
|
185
|
-
unit: "percent",
|
|
186
|
-
operators: [
|
|
187
|
-
"equal",
|
|
188
|
-
"less",
|
|
189
|
-
"less_or_equal",
|
|
190
|
-
"greater",
|
|
191
|
-
"greater_or_equal",
|
|
192
|
-
],
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
key: "userMetric.min_debt_interest_rate",
|
|
196
|
-
label: "Min Debt Interest Rate",
|
|
197
|
-
type: "number",
|
|
198
|
-
unit: "percent",
|
|
199
|
-
operators: [
|
|
200
|
-
"equal",
|
|
201
|
-
"less",
|
|
202
|
-
"less_or_equal",
|
|
203
|
-
"greater",
|
|
204
|
-
"greater_or_equal",
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
];
|
|
208
|
-
|
|
209
|
-
const ALL_NUMERIC_OPERATORS: AlertQueryOperator[] = [
|
|
210
|
-
"equal",
|
|
211
|
-
"not_equal",
|
|
212
|
-
"less",
|
|
213
|
-
"less_or_equal",
|
|
214
|
-
"greater",
|
|
215
|
-
"greater_or_equal",
|
|
216
|
-
"between",
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
const BOOLEAN_OPERATORS: AlertQueryOperator[] = ["equal"];
|
|
220
|
-
|
|
221
|
-
const OPERATOR_LABELS: Record<AlertQueryOperator, string> = {
|
|
222
|
-
equal: "=",
|
|
223
|
-
not_equal: "≠",
|
|
224
|
-
less: "<",
|
|
225
|
-
less_or_equal: "≤",
|
|
226
|
-
greater: ">",
|
|
227
|
-
greater_or_equal: "≥",
|
|
228
|
-
between: "between",
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const SEVERITY_LABELS: Record<ContactAlertSeverity, string> = {
|
|
232
|
-
INSIGHT: "Insight",
|
|
233
|
-
WATCH: "Watch",
|
|
234
|
-
NEED_ACTION: "Need Action",
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const EMPTY_RULE = (): AlertQueryRule => ({
|
|
238
|
-
id: Math.random().toString(36).slice(2),
|
|
239
|
-
field: ALERT_QUERY_FIELDS[0].key,
|
|
240
|
-
operator: "greater_or_equal",
|
|
241
|
-
value: "",
|
|
242
|
-
value2: "",
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
// Helpers
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
|
|
249
|
-
/** Longest label in an array of strings. */
|
|
250
|
-
function longestOf(labels: string[]): string {
|
|
251
|
-
return labels.reduce((a, b) => (a.length >= b.length ? a : b), "");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Operators available across all fields — used to lock the operator-select width. */
|
|
255
|
-
function allOperatorLabels(fields: AlertQueryField[]): string[] {
|
|
256
|
-
const ops = new Set<AlertQueryOperator>();
|
|
257
|
-
for (const f of fields) {
|
|
258
|
-
const fieldOps =
|
|
259
|
-
f.type === "boolean"
|
|
260
|
-
? BOOLEAN_OPERATORS
|
|
261
|
-
: (f.operators ?? ALL_NUMERIC_OPERATORS);
|
|
262
|
-
fieldOps.forEach((op) => ops.add(op));
|
|
263
|
-
}
|
|
264
|
-
return [...ops].map((op) => OPERATOR_LABELS[op]);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Add thousand separators to a raw digit string. */
|
|
268
|
-
function formatWithCommas(raw: string): string {
|
|
269
|
-
if (!raw) return "";
|
|
270
|
-
const [int, dec] = raw.split(".");
|
|
271
|
-
const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
272
|
-
return dec !== undefined ? `${formatted}.${dec}` : formatted;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Strip thousand separators; keep digits and at most one decimal point. */
|
|
276
|
-
function parseCommas(display: string): string {
|
|
277
|
-
return display.replace(/[^\d.]/g, "").replace(/(\..*)\./g, "$1");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
// SelectAutoWidth
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Locks a Select trigger to the width of its longest option.
|
|
286
|
-
*
|
|
287
|
-
* Ghost block + absolute overlay pattern:
|
|
288
|
-
* - An invisible <span> with longestLabel sizes the container (no circular CSS dependency).
|
|
289
|
-
* - The trigger sits in an absolute overlay that fills the container.
|
|
290
|
-
*
|
|
291
|
-
* Ghost padding mirrors trigger chrome:
|
|
292
|
-
* left = px-3 (12 px)
|
|
293
|
-
* right = gap-2 (8 px) + chevron size-4 (16 px) + px-3 (12 px) = 36 px → pr-10 + 4 px buffer
|
|
294
|
-
* text-body-medium matches the SelectTrigger font (1 rem / 16 px).
|
|
295
|
-
* h-8 matches trigger size="sm".
|
|
296
|
-
*/
|
|
297
|
-
function SelectAutoWidth({
|
|
298
|
-
longestLabel,
|
|
299
|
-
children,
|
|
300
|
-
}: {
|
|
301
|
-
longestLabel: string;
|
|
302
|
-
children: React.ReactNode;
|
|
303
|
-
}) {
|
|
304
|
-
return (
|
|
305
|
-
<div className="relative inline-block shrink-0">
|
|
306
|
-
<span
|
|
307
|
-
aria-hidden
|
|
308
|
-
className="invisible block h-8 whitespace-nowrap pl-3 pr-10 text-body-medium"
|
|
309
|
-
>
|
|
310
|
-
{longestLabel}
|
|
311
|
-
</span>
|
|
312
|
-
<div className="absolute inset-0">{children}</div>
|
|
313
|
-
</div>
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
// ValueInput
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
|
|
321
|
-
function ValueInput({
|
|
322
|
-
value,
|
|
323
|
-
onChange,
|
|
324
|
-
unit,
|
|
325
|
-
placeholder = "0",
|
|
326
|
-
}: {
|
|
327
|
-
value: string;
|
|
328
|
-
onChange: (v: string) => void;
|
|
329
|
-
unit?: "dollar" | "percent";
|
|
330
|
-
placeholder?: string;
|
|
331
|
-
}) {
|
|
332
|
-
return (
|
|
333
|
-
<InputGroup className="w-36">
|
|
334
|
-
{unit === "dollar" && (
|
|
335
|
-
<InputGroupAddon align="inline-start">
|
|
336
|
-
<InputGroupText>$</InputGroupText>
|
|
337
|
-
</InputGroupAddon>
|
|
338
|
-
)}
|
|
339
|
-
<InputGroupInput
|
|
340
|
-
type="text"
|
|
341
|
-
inputMode="numeric"
|
|
342
|
-
value={formatWithCommas(value)}
|
|
343
|
-
onChange={(e) => onChange(parseCommas(e.target.value))}
|
|
344
|
-
placeholder={placeholder}
|
|
345
|
-
/>
|
|
346
|
-
{unit === "percent" && (
|
|
347
|
-
<InputGroupAddon align="inline-end">
|
|
348
|
-
<InputGroupText>%</InputGroupText>
|
|
349
|
-
</InputGroupAddon>
|
|
350
|
-
)}
|
|
351
|
-
</InputGroup>
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
// QueryRuleRow
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
|
|
359
|
-
function QueryRuleRow({
|
|
360
|
-
rule,
|
|
361
|
-
fields,
|
|
362
|
-
longestFieldLabel,
|
|
363
|
-
longestOperatorLabel,
|
|
364
|
-
onUpdate,
|
|
365
|
-
onRemove,
|
|
366
|
-
removable,
|
|
367
|
-
}: {
|
|
368
|
-
rule: AlertQueryRule;
|
|
369
|
-
fields: AlertQueryField[];
|
|
370
|
-
longestFieldLabel: string;
|
|
371
|
-
longestOperatorLabel: string;
|
|
372
|
-
onUpdate: (updated: AlertQueryRule) => void;
|
|
373
|
-
onRemove: () => void;
|
|
374
|
-
removable: boolean;
|
|
375
|
-
}) {
|
|
376
|
-
const fieldDef = fields.find((f) => f.key === rule.field) ?? fields[0];
|
|
377
|
-
const isBooleanField = fieldDef.type === "boolean";
|
|
378
|
-
const availableOperators = isBooleanField
|
|
379
|
-
? BOOLEAN_OPERATORS
|
|
380
|
-
: (fieldDef.operators ?? ALL_NUMERIC_OPERATORS);
|
|
381
|
-
|
|
382
|
-
const update = (patch: Partial<AlertQueryRule>) =>
|
|
383
|
-
onUpdate({ ...rule, ...patch });
|
|
384
|
-
|
|
385
|
-
function handleFieldChange(key: string) {
|
|
386
|
-
const newField = fields.find((f) => f.key === key);
|
|
387
|
-
const newOps =
|
|
388
|
-
newField?.type === "boolean"
|
|
389
|
-
? BOOLEAN_OPERATORS
|
|
390
|
-
: (newField?.operators ?? ALL_NUMERIC_OPERATORS);
|
|
391
|
-
const validOp = newOps.includes(rule.operator) ? rule.operator : newOps[0];
|
|
392
|
-
update({ field: key, operator: validOp, value: "", value2: "" });
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return (
|
|
396
|
-
<div className="flex flex-wrap items-center gap-1.5 border border-border bg-muted/20 p-2">
|
|
397
|
-
{/* Field */}
|
|
398
|
-
<SelectAutoWidth longestLabel={longestFieldLabel}>
|
|
399
|
-
<Select value={rule.field} onValueChange={handleFieldChange}>
|
|
400
|
-
<SelectTrigger size="sm" className="w-full">
|
|
401
|
-
<SelectValue>
|
|
402
|
-
{fields.find((f) => f.key === rule.field)?.label ?? rule.field}
|
|
403
|
-
</SelectValue>
|
|
404
|
-
</SelectTrigger>
|
|
405
|
-
<SelectContent>
|
|
406
|
-
{fields.map((f) => (
|
|
407
|
-
<SelectItem key={f.key} value={f.key}>
|
|
408
|
-
{f.label}
|
|
409
|
-
</SelectItem>
|
|
410
|
-
))}
|
|
411
|
-
</SelectContent>
|
|
412
|
-
</Select>
|
|
413
|
-
</SelectAutoWidth>
|
|
414
|
-
|
|
415
|
-
{/* Operator */}
|
|
416
|
-
<SelectAutoWidth longestLabel={longestOperatorLabel}>
|
|
417
|
-
<Select
|
|
418
|
-
value={rule.operator}
|
|
419
|
-
onValueChange={(v) => update({ operator: v as AlertQueryOperator })}
|
|
420
|
-
>
|
|
421
|
-
<SelectTrigger size="sm" className="w-full">
|
|
422
|
-
<SelectValue>{OPERATOR_LABELS[rule.operator]}</SelectValue>
|
|
423
|
-
</SelectTrigger>
|
|
424
|
-
<SelectContent>
|
|
425
|
-
{availableOperators.map((op) => (
|
|
426
|
-
<SelectItem key={op} value={op}>
|
|
427
|
-
{OPERATOR_LABELS[op]}
|
|
428
|
-
</SelectItem>
|
|
429
|
-
))}
|
|
430
|
-
</SelectContent>
|
|
431
|
-
</Select>
|
|
432
|
-
</SelectAutoWidth>
|
|
433
|
-
|
|
434
|
-
{/* Value */}
|
|
435
|
-
{isBooleanField ? (
|
|
436
|
-
<Select
|
|
437
|
-
value={rule.value || "true"}
|
|
438
|
-
onValueChange={(v) => update({ value: v })}
|
|
439
|
-
>
|
|
440
|
-
<SelectTrigger size="sm" className="w-36">
|
|
441
|
-
<SelectValue>
|
|
442
|
-
{(rule.value || "true") === "false" ? "No" : "Yes"}
|
|
443
|
-
</SelectValue>
|
|
444
|
-
</SelectTrigger>
|
|
445
|
-
<SelectContent>
|
|
446
|
-
<SelectItem value="true">Yes</SelectItem>
|
|
447
|
-
<SelectItem value="false">No</SelectItem>
|
|
448
|
-
</SelectContent>
|
|
449
|
-
</Select>
|
|
450
|
-
) : (
|
|
451
|
-
<>
|
|
452
|
-
<ValueInput
|
|
453
|
-
value={rule.value}
|
|
454
|
-
onChange={(v) => update({ value: v })}
|
|
455
|
-
unit={fieldDef.unit}
|
|
456
|
-
/>
|
|
457
|
-
{rule.operator === "between" && (
|
|
458
|
-
<>
|
|
459
|
-
<span className="text-xs text-muted-foreground">and</span>
|
|
460
|
-
<ValueInput
|
|
461
|
-
value={rule.value2 ?? ""}
|
|
462
|
-
onChange={(v) => update({ value2: v })}
|
|
463
|
-
unit={fieldDef.unit}
|
|
464
|
-
/>
|
|
465
|
-
</>
|
|
466
|
-
)}
|
|
467
|
-
</>
|
|
468
|
-
)}
|
|
469
|
-
|
|
470
|
-
{/* Remove */}
|
|
471
|
-
<Button
|
|
472
|
-
type="button"
|
|
473
|
-
variant="ghost"
|
|
474
|
-
size="sm"
|
|
475
|
-
onClick={onRemove}
|
|
476
|
-
disabled={!removable}
|
|
477
|
-
aria-label="Remove rule"
|
|
478
|
-
className="ml-auto"
|
|
479
|
-
>
|
|
480
|
-
<Trash2Icon />
|
|
481
|
-
</Button>
|
|
482
|
-
</div>
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// ---------------------------------------------------------------------------
|
|
487
|
-
// ContactAlertQueryBuilder
|
|
488
|
-
// ---------------------------------------------------------------------------
|
|
489
|
-
|
|
490
|
-
export function ContactAlertQueryBuilder({
|
|
491
|
-
value,
|
|
492
|
-
onChange,
|
|
493
|
-
fields,
|
|
494
|
-
className,
|
|
495
|
-
}: ContactAlertQueryBuilderProps) {
|
|
496
|
-
const longestFieldLabel = React.useMemo(
|
|
497
|
-
() => longestOf(fields.map((f) => f.label)),
|
|
498
|
-
[fields],
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
const longestOperatorLabel = React.useMemo(
|
|
502
|
-
() => longestOf(allOperatorLabels(fields)),
|
|
503
|
-
[fields],
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
function updateRule(index: number, updated: AlertQueryRule) {
|
|
507
|
-
onChange({
|
|
508
|
-
...value,
|
|
509
|
-
rules: value.rules.map((r, i) => (i === index ? updated : r)),
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function removeRule(index: number) {
|
|
514
|
-
if (value.rules.length <= 1) return;
|
|
515
|
-
const rules = value.rules.filter((_, i) => i !== index);
|
|
516
|
-
const opIdx = Math.min(index, value.logicalOps.length - 1);
|
|
517
|
-
const logicalOps = value.logicalOps.filter((_, i) => i !== opIdx);
|
|
518
|
-
onChange({ rules, logicalOps });
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function addRule() {
|
|
522
|
-
onChange({
|
|
523
|
-
rules: [...value.rules, EMPTY_RULE()],
|
|
524
|
-
logicalOps: [...value.logicalOps, "AND"],
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function setLogicalOp(index: number, op: AlertQueryCombinator) {
|
|
529
|
-
onChange({
|
|
530
|
-
...value,
|
|
531
|
-
logicalOps: value.logicalOps.map((o, i) => (i === index ? op : o)),
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return (
|
|
536
|
-
<div className={cn("flex flex-col gap-1.5", className)}>
|
|
537
|
-
{value.rules.map((rule, idx) => (
|
|
538
|
-
<React.Fragment key={rule.id}>
|
|
539
|
-
<QueryRuleRow
|
|
540
|
-
rule={rule}
|
|
541
|
-
fields={fields}
|
|
542
|
-
longestFieldLabel={longestFieldLabel}
|
|
543
|
-
longestOperatorLabel={longestOperatorLabel}
|
|
544
|
-
onUpdate={(updated) => updateRule(idx, updated)}
|
|
545
|
-
onRemove={() => removeRule(idx)}
|
|
546
|
-
removable={value.rules.length > 1}
|
|
547
|
-
/>
|
|
548
|
-
{idx < value.rules.length - 1 && (
|
|
549
|
-
<div className="flex items-center gap-2 pl-1">
|
|
550
|
-
<ToggleGroup
|
|
551
|
-
type="single"
|
|
552
|
-
variant="outline"
|
|
553
|
-
size="sm"
|
|
554
|
-
value={value.logicalOps[idx] ?? "AND"}
|
|
555
|
-
onValueChange={(v) =>
|
|
556
|
-
v && setLogicalOp(idx, v as AlertQueryCombinator)
|
|
557
|
-
}
|
|
558
|
-
>
|
|
559
|
-
<ToggleGroupItem value="AND">AND</ToggleGroupItem>
|
|
560
|
-
<ToggleGroupItem value="OR">OR</ToggleGroupItem>
|
|
561
|
-
</ToggleGroup>
|
|
562
|
-
</div>
|
|
563
|
-
)}
|
|
564
|
-
</React.Fragment>
|
|
565
|
-
))}
|
|
566
|
-
|
|
567
|
-
<Button
|
|
568
|
-
type="button"
|
|
569
|
-
variant="ghost"
|
|
570
|
-
size="sm"
|
|
571
|
-
onClick={addRule}
|
|
572
|
-
className="w-fit"
|
|
573
|
-
>
|
|
574
|
-
<PlusIcon />
|
|
575
|
-
Add rule
|
|
576
|
-
</Button>
|
|
577
|
-
</div>
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// ---------------------------------------------------------------------------
|
|
582
|
-
// ContactAlertDialog
|
|
583
|
-
// ---------------------------------------------------------------------------
|
|
584
|
-
|
|
585
|
-
export function ContactAlertDialog({
|
|
586
|
-
open,
|
|
587
|
-
onOpenChange,
|
|
588
|
-
mode = "create",
|
|
589
|
-
initialName = "",
|
|
590
|
-
initialSeverity = "NEED_ACTION",
|
|
591
|
-
initialQuery,
|
|
592
|
-
isCompanyAdmin = false,
|
|
593
|
-
initialShareAcrossCompany = false,
|
|
594
|
-
onSave,
|
|
595
|
-
isLoading = false,
|
|
596
|
-
className,
|
|
597
|
-
}: ContactAlertDialogProps) {
|
|
598
|
-
const [name, setName] = React.useState(initialName);
|
|
599
|
-
const [severity, setSeverity] =
|
|
600
|
-
React.useState<ContactAlertSeverity>(initialSeverity);
|
|
601
|
-
const [query, setQuery] = React.useState<AlertQueryGroup>(
|
|
602
|
-
initialQuery ?? { rules: [EMPTY_RULE()], logicalOps: [] },
|
|
603
|
-
);
|
|
604
|
-
const [shareAcrossCompany, setShareAcrossCompany] = React.useState(
|
|
605
|
-
initialShareAcrossCompany,
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
React.useEffect(() => {
|
|
609
|
-
if (open) {
|
|
610
|
-
setName(initialName);
|
|
611
|
-
setSeverity(initialSeverity);
|
|
612
|
-
setQuery(initialQuery ?? { rules: [EMPTY_RULE()], logicalOps: [] });
|
|
613
|
-
setShareAcrossCompany(initialShareAcrossCompany);
|
|
614
|
-
}
|
|
615
|
-
}, [open]);
|
|
616
|
-
|
|
617
|
-
const hasValidRule = query.rules.some((r) => {
|
|
618
|
-
const fieldDef = ALERT_QUERY_FIELDS.find((f) => f.key === r.field);
|
|
619
|
-
return fieldDef?.type === "boolean" || r.value !== "";
|
|
620
|
-
});
|
|
621
|
-
const canSave = name.trim().length > 0 && hasValidRule && !isLoading;
|
|
622
|
-
|
|
623
|
-
function handleSave() {
|
|
624
|
-
if (!canSave) return;
|
|
625
|
-
onSave({ name: name.trim(), severity, query, shareAcrossCompany });
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
return (
|
|
629
|
-
<Dialog open={open} onOpenChange={isLoading ? undefined : onOpenChange}>
|
|
630
|
-
<DialogContent size="lg" className={className}>
|
|
631
|
-
<DialogHeader>
|
|
632
|
-
<DialogTitle>
|
|
633
|
-
{mode === "edit" ? "Update Alert" : "Create Alert"}
|
|
634
|
-
</DialogTitle>
|
|
635
|
-
</DialogHeader>
|
|
636
|
-
|
|
637
|
-
{/* Severity */}
|
|
638
|
-
<div className="flex flex-col gap-1.5">
|
|
639
|
-
<p className="text-sm font-medium text-foreground">
|
|
640
|
-
Alert trigger severity
|
|
641
|
-
</p>
|
|
642
|
-
<ToggleGroup
|
|
643
|
-
type="single"
|
|
644
|
-
variant="outline"
|
|
645
|
-
size="sm"
|
|
646
|
-
value={severity}
|
|
647
|
-
onValueChange={(v) => v && setSeverity(v as ContactAlertSeverity)}
|
|
648
|
-
>
|
|
649
|
-
{(
|
|
650
|
-
["NEED_ACTION", "WATCH", "INSIGHT"] as ContactAlertSeverity[]
|
|
651
|
-
).map((s) => (
|
|
652
|
-
<ToggleGroupItem key={s} value={s}>
|
|
653
|
-
{SEVERITY_LABELS[s]}
|
|
654
|
-
</ToggleGroupItem>
|
|
655
|
-
))}
|
|
656
|
-
</ToggleGroup>
|
|
657
|
-
</div>
|
|
658
|
-
|
|
659
|
-
{/* Name */}
|
|
660
|
-
<Field>
|
|
661
|
-
<FieldLabel>Alert name</FieldLabel>
|
|
662
|
-
<Input
|
|
663
|
-
value={name}
|
|
664
|
-
onChange={(e) => setName(e.target.value)}
|
|
665
|
-
placeholder="e.g. High equity opportunity"
|
|
666
|
-
/>
|
|
667
|
-
</Field>
|
|
668
|
-
|
|
669
|
-
{/* Share across company */}
|
|
670
|
-
{isCompanyAdmin && (
|
|
671
|
-
<div className="flex items-center gap-2">
|
|
672
|
-
<Checkbox
|
|
673
|
-
id="alert-share"
|
|
674
|
-
checked={shareAcrossCompany}
|
|
675
|
-
onCheckedChange={(v) => setShareAcrossCompany(!!v)}
|
|
676
|
-
/>
|
|
677
|
-
<Label htmlFor="alert-share" className="cursor-pointer font-normal">
|
|
678
|
-
Share across company
|
|
679
|
-
</Label>
|
|
680
|
-
</div>
|
|
681
|
-
)}
|
|
682
|
-
|
|
683
|
-
{/* Query builder */}
|
|
684
|
-
<div className="flex flex-col gap-1.5">
|
|
685
|
-
<p className="text-sm font-medium text-foreground">
|
|
686
|
-
Filter conditions
|
|
687
|
-
</p>
|
|
688
|
-
<ContactAlertQueryBuilder
|
|
689
|
-
value={query}
|
|
690
|
-
onChange={setQuery}
|
|
691
|
-
fields={ALERT_QUERY_FIELDS}
|
|
692
|
-
/>
|
|
693
|
-
</div>
|
|
694
|
-
|
|
695
|
-
<DialogFooter>
|
|
696
|
-
<Button
|
|
697
|
-
variant="outline"
|
|
698
|
-
onClick={() => onOpenChange(false)}
|
|
699
|
-
disabled={isLoading}
|
|
700
|
-
>
|
|
701
|
-
Cancel
|
|
702
|
-
</Button>
|
|
703
|
-
<Button disabled={!canSave} onClick={handleSave}>
|
|
704
|
-
{mode === "edit" ? "Update Alert" : "Create Alert"}
|
|
705
|
-
</Button>
|
|
706
|
-
</DialogFooter>
|
|
707
|
-
</DialogContent>
|
|
708
|
-
</Dialog>
|
|
709
|
-
);
|
|
710
|
-
}
|