@wealthx/shadcn 1.5.0 → 1.5.2

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 (36) hide show
  1. package/.turbo/turbo-build.log +119 -119
  2. package/CHANGELOG.md +12 -0
  3. package/dist/chunk-G2EWIP2N.mjs +960 -0
  4. package/dist/{chunk-MHHA7QGO.mjs → chunk-ODO6BUOF.mjs} +1 -1
  5. package/dist/chunk-PX4M67XQ.mjs +301 -0
  6. package/dist/{chunk-FYUSF5KO.mjs → chunk-QRVEI6J3.mjs} +1 -1
  7. package/dist/{chunk-42NEC57Y.mjs → chunk-RAKBWNQH.mjs} +272 -3
  8. package/dist/components/ui/{contact-alert-dialog.js → contact-alert-dialog/index.js} +1029 -593
  9. package/dist/components/ui/contact-alert-dialog/index.mjs +31 -0
  10. package/dist/components/ui/file-preview-dialog.js +407 -100
  11. package/dist/components/ui/file-preview-dialog.mjs +3 -1
  12. package/dist/components/ui/kanban-column.js +408 -113
  13. package/dist/components/ui/kanban-column.mjs +3 -2
  14. package/dist/components/ui/opportunity-card.js +383 -88
  15. package/dist/components/ui/opportunity-card.mjs +2 -1
  16. package/dist/components/ui/pipeline-board.js +424 -129
  17. package/dist/components/ui/pipeline-board.mjs +4 -3
  18. package/dist/index.js +3081 -2282
  19. package/dist/index.mjs +39 -35
  20. package/dist/styles.css +1 -1
  21. package/package.json +6 -5
  22. package/src/components/index.tsx +3 -2
  23. package/src/components/ui/contact-alert-dialog/builder-ui.tsx +556 -0
  24. package/src/components/ui/contact-alert-dialog/config.ts +262 -0
  25. package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +214 -0
  26. package/src/components/ui/contact-alert-dialog/index.tsx +15 -0
  27. package/src/components/ui/contact-alert-dialog/types.ts +61 -0
  28. package/src/components/ui/contact-alert-dialog/utils.ts +93 -0
  29. package/src/components/ui/file-preview-dialog.tsx +299 -99
  30. package/src/components/ui/opportunity-card.tsx +328 -1
  31. package/src/styles/styles-css.ts +1 -1
  32. package/tsup.config.ts +1 -1
  33. package/dist/chunk-5WMFKQZ6.mjs +0 -180
  34. package/dist/chunk-Y24TXIFJ.mjs +0 -518
  35. package/dist/components/ui/contact-alert-dialog.mjs +0 -27
  36. package/src/components/ui/contact-alert-dialog.tsx +0 -710
@@ -0,0 +1,262 @@
1
+ import {
2
+ BasicConfig,
3
+ Utils as QbUtils,
4
+ type Config,
5
+ type ImmutableTree,
6
+ type JsonGroup,
7
+ type JsonItem,
8
+ } from "@react-awesome-query-builder/ui";
9
+ import type { AlertQueryField, AlertQueryOperator } from "./types";
10
+
11
+ export const ALERT_QUERY_FIELDS: AlertQueryField[] = [
12
+ {
13
+ key: "userMetric.max_loan_amount",
14
+ label: "Borrowing Capacity",
15
+ type: "number",
16
+ unit: "dollar",
17
+ operators: [
18
+ "equal",
19
+ "less",
20
+ "less_or_equal",
21
+ "greater",
22
+ "greater_or_equal",
23
+ "between",
24
+ ],
25
+ },
26
+ {
27
+ key: "userMetric.debt_outstanding",
28
+ label: "Outstanding Debt",
29
+ type: "number",
30
+ unit: "dollar",
31
+ operators: [
32
+ "equal",
33
+ "less",
34
+ "less_or_equal",
35
+ "greater",
36
+ "greater_or_equal",
37
+ ],
38
+ },
39
+ {
40
+ key: "userMetric.lvr",
41
+ label: "Current LVR",
42
+ type: "number",
43
+ unit: "percent",
44
+ operators: [
45
+ "equal",
46
+ "less",
47
+ "less_or_equal",
48
+ "greater",
49
+ "greater_or_equal",
50
+ ],
51
+ },
52
+ {
53
+ key: "userMetric.has_met_buying_goal",
54
+ label: "Has Met Buying Goal",
55
+ type: "boolean",
56
+ },
57
+ {
58
+ key: "userMetric.excess_monthly_surplus",
59
+ label: "Excess Monthly Surplus",
60
+ type: "number",
61
+ unit: "dollar",
62
+ operators: [
63
+ "equal",
64
+ "less",
65
+ "less_or_equal",
66
+ "greater",
67
+ "greater_or_equal",
68
+ ],
69
+ },
70
+ {
71
+ key: "userMetric.equity",
72
+ label: "Equity Amount",
73
+ type: "number",
74
+ unit: "dollar",
75
+ operators: [
76
+ "equal",
77
+ "less",
78
+ "less_or_equal",
79
+ "greater",
80
+ "greater_or_equal",
81
+ ],
82
+ },
83
+ {
84
+ key: "userMetric.max_debt_interest_rate",
85
+ label: "Max Debt Interest Rate",
86
+ type: "number",
87
+ unit: "percent",
88
+ operators: [
89
+ "equal",
90
+ "less",
91
+ "less_or_equal",
92
+ "greater",
93
+ "greater_or_equal",
94
+ ],
95
+ },
96
+ {
97
+ key: "userMetric.min_debt_interest_rate",
98
+ label: "Min Debt Interest Rate",
99
+ type: "number",
100
+ unit: "percent",
101
+ operators: [
102
+ "equal",
103
+ "less",
104
+ "less_or_equal",
105
+ "greater",
106
+ "greater_or_equal",
107
+ ],
108
+ },
109
+ ];
110
+
111
+ export const ALL_NUMERIC_OPERATORS: AlertQueryOperator[] = [
112
+ "equal",
113
+ "not_equal",
114
+ "less",
115
+ "less_or_equal",
116
+ "greater",
117
+ "greater_or_equal",
118
+ "between",
119
+ ];
120
+
121
+ export const BOOLEAN_OPERATORS: AlertQueryOperator[] = ["equal"];
122
+
123
+ export const OPERATOR_LABELS: Record<AlertQueryOperator, string> = {
124
+ equal: "=",
125
+ not_equal: "≠",
126
+ less: "<",
127
+ less_or_equal: "≤",
128
+ greater: ">",
129
+ greater_or_equal: "≥",
130
+ between: "between",
131
+ };
132
+
133
+ export const SEVERITY_LABELS = {
134
+ INSIGHT: "Insight",
135
+ WATCH: "Watch",
136
+ NEED_ACTION: "Need Action",
137
+ } as const;
138
+
139
+ // react-awesome-query-builder config — drives ImmutableTree state.
140
+ export const QB_CONFIG: Config = {
141
+ ...BasicConfig,
142
+ fields: {
143
+ "userMetric.max_loan_amount": {
144
+ label: "Borrowing Capacity",
145
+ type: "number",
146
+ operators: [
147
+ "equal",
148
+ "less",
149
+ "less_or_equal",
150
+ "greater",
151
+ "greater_or_equal",
152
+ "between",
153
+ ],
154
+ valueSources: ["value"],
155
+ },
156
+ "userMetric.debt_outstanding": {
157
+ label: "Outstanding Debt",
158
+ type: "number",
159
+ operators: [
160
+ "equal",
161
+ "less",
162
+ "less_or_equal",
163
+ "greater",
164
+ "greater_or_equal",
165
+ ],
166
+ valueSources: ["value"],
167
+ },
168
+ "userMetric.lvr": {
169
+ label: "Current LVR",
170
+ type: "number",
171
+ operators: [
172
+ "equal",
173
+ "less",
174
+ "less_or_equal",
175
+ "greater",
176
+ "greater_or_equal",
177
+ ],
178
+ valueSources: ["value"],
179
+ },
180
+ "userMetric.has_met_buying_goal": {
181
+ label: "Has Met Buying Goal",
182
+ type: "boolean",
183
+ operators: ["equal"],
184
+ valueSources: ["value"],
185
+ },
186
+ "userMetric.excess_monthly_surplus": {
187
+ label: "Excess Monthly Surplus",
188
+ type: "number",
189
+ operators: [
190
+ "equal",
191
+ "less",
192
+ "less_or_equal",
193
+ "greater",
194
+ "greater_or_equal",
195
+ ],
196
+ valueSources: ["value"],
197
+ },
198
+ "userMetric.equity": {
199
+ label: "Equity Amount",
200
+ type: "number",
201
+ operators: [
202
+ "equal",
203
+ "less",
204
+ "less_or_equal",
205
+ "greater",
206
+ "greater_or_equal",
207
+ ],
208
+ valueSources: ["value"],
209
+ },
210
+ "userMetric.max_debt_interest_rate": {
211
+ label: "Max Debt Interest Rate",
212
+ type: "number",
213
+ operators: [
214
+ "equal",
215
+ "less",
216
+ "less_or_equal",
217
+ "greater",
218
+ "greater_or_equal",
219
+ ],
220
+ valueSources: ["value"],
221
+ },
222
+ "userMetric.min_debt_interest_rate": {
223
+ label: "Min Debt Interest Rate",
224
+ type: "number",
225
+ operators: [
226
+ "equal",
227
+ "less",
228
+ "less_or_equal",
229
+ "greater",
230
+ "greater_or_equal",
231
+ ],
232
+ valueSources: ["value"],
233
+ },
234
+ },
235
+ };
236
+
237
+ // Default query — children1 in array form (matches JsonGroup type).
238
+ export const EMPTY_QUERY_VALUE: JsonGroup = {
239
+ id: "root",
240
+ type: "group",
241
+ properties: { conjunction: "AND", not: false },
242
+ children1: [
243
+ {
244
+ id: "rule-init",
245
+ type: "rule",
246
+ properties: {
247
+ field: "userMetric.max_loan_amount",
248
+ operator: "greater_or_equal",
249
+ value: [null],
250
+ valueSrc: ["value"],
251
+ },
252
+ } as JsonItem,
253
+ ],
254
+ };
255
+
256
+ /** Convert a JsonGroup to an ImmutableTree usable by the query builder. */
257
+ export function createAlertTree(query?: JsonGroup | null): ImmutableTree {
258
+ const q = query ?? EMPTY_QUERY_VALUE;
259
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
260
+ const loaded = QbUtils.loadTree(q as any);
261
+ return QbUtils.sanitizeTree(loaded, QB_CONFIG).fixedTree;
262
+ }
@@ -0,0 +1,214 @@
1
+ import * as React from "react";
2
+ import {
3
+ Query,
4
+ Utils as QbUtils,
5
+ type ImmutableTree,
6
+ } from "@react-awesome-query-builder/ui";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogFooter,
13
+ } from "@/components/ui/dialog";
14
+ import { Button } from "@/components/ui/button";
15
+ import { Input } from "@/components/ui/input";
16
+ import { Checkbox } from "@/components/ui/checkbox";
17
+ import { Label } from "@/components/ui/label";
18
+ import { Field, FieldLabel } from "@/components/ui/field";
19
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
20
+ import type {
21
+ ContactAlertDialogProps,
22
+ ContactAlertQueryBuilderProps,
23
+ ContactAlertSeverity,
24
+ } from "./types";
25
+ import { AlertSharingType } from "./types";
26
+ import {
27
+ ALERT_QUERY_FIELDS,
28
+ QB_CONFIG,
29
+ SEVERITY_LABELS,
30
+ createAlertTree,
31
+ } from "./config";
32
+ import { isTreeValid } from "./utils";
33
+ import { CustomBuilderUI } from "./builder-ui";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // ContactAlertQueryBuilder
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export function ContactAlertQueryBuilder({
40
+ value,
41
+ onChange,
42
+ fields = ALERT_QUERY_FIELDS,
43
+ className,
44
+ }: ContactAlertQueryBuilderProps) {
45
+ const defaultOpenItems = React.useMemo(() => {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ const json = QbUtils.getTree(value) as any;
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ return (json?.children1 ?? []).map((c: any) => c.id ?? "").filter(Boolean);
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, []);
52
+
53
+ const renderBuilder = React.useCallback(
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ (props: any) => (
56
+ <CustomBuilderUI
57
+ tree={props.tree}
58
+ actions={props.actions}
59
+ fields={fields}
60
+ defaultOpenItems={defaultOpenItems}
61
+ className={className}
62
+ />
63
+ ),
64
+ [fields, defaultOpenItems, className],
65
+ );
66
+
67
+ return (
68
+ <Query
69
+ {...QB_CONFIG}
70
+ value={value}
71
+ onChange={(newTree) => onChange(newTree)}
72
+ renderBuilder={renderBuilder}
73
+ />
74
+ );
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // ContactAlertDialog
79
+ // ---------------------------------------------------------------------------
80
+
81
+ export function ContactAlertDialog({
82
+ open,
83
+ onOpenChange,
84
+ mode = "create",
85
+ initialName = "",
86
+ initialSeverity = "NEED_ACTION",
87
+ initialQuery,
88
+ isCompanyAdmin = false,
89
+ initialShareAcrossCompany = false,
90
+ onSave,
91
+ isLoading = false,
92
+ className,
93
+ }: ContactAlertDialogProps) {
94
+ const [name, setName] = React.useState(initialName);
95
+ const [severity, setSeverity] =
96
+ React.useState<ContactAlertSeverity>(initialSeverity);
97
+ const [tree, setTree] = React.useState<ImmutableTree>(() =>
98
+ createAlertTree(initialQuery),
99
+ );
100
+ const [shareAcrossCompany, setShareAcrossCompany] = React.useState(
101
+ initialShareAcrossCompany,
102
+ );
103
+
104
+ React.useEffect(() => {
105
+ if (open) {
106
+ setName(initialName);
107
+ setSeverity(initialSeverity);
108
+ setTree(createAlertTree(initialQuery));
109
+ setShareAcrossCompany(initialShareAcrossCompany);
110
+ }
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
+ }, [open]);
113
+
114
+ const hasValidRule = React.useMemo(() => isTreeValid(tree), [tree]);
115
+ const canSave = name.trim().length > 0 && hasValidRule && !isLoading;
116
+
117
+ function handleSave() {
118
+ if (!canSave) return;
119
+ onSave({
120
+ name: name.trim(),
121
+ severity,
122
+ filterSegment: QbUtils.sanitizeTree(tree, QB_CONFIG).fixedTree,
123
+ sharingType: shareAcrossCompany
124
+ ? AlertSharingType.COMPANY
125
+ : AlertSharingType.PRIVATE,
126
+ });
127
+ }
128
+
129
+ return (
130
+ <Dialog open={open} onOpenChange={isLoading ? undefined : onOpenChange}>
131
+ <DialogContent size="2xl" className={className}>
132
+ <DialogHeader>
133
+ <DialogTitle>
134
+ {mode === "edit" ? "Update Alert" : "Create Alert"}
135
+ </DialogTitle>
136
+ </DialogHeader>
137
+
138
+ {/* Severity */}
139
+ <div className="flex flex-col gap-1.5">
140
+ <p className="text-sm font-medium text-foreground">
141
+ Alert trigger severity
142
+ </p>
143
+ <ToggleGroup
144
+ type="single"
145
+ variant="outline"
146
+ size="sm"
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ value={severity as any}
149
+ onValueChange={(v) =>
150
+ v && setSeverity(v as unknown as ContactAlertSeverity)
151
+ }
152
+ >
153
+ {(
154
+ ["NEED_ACTION", "WATCH", "INSIGHT"] as ContactAlertSeverity[]
155
+ ).map((s) => (
156
+ <ToggleGroupItem key={s} value={s}>
157
+ {SEVERITY_LABELS[s]}
158
+ </ToggleGroupItem>
159
+ ))}
160
+ </ToggleGroup>
161
+ </div>
162
+
163
+ {/* Name */}
164
+ <Field>
165
+ <FieldLabel>Alert name</FieldLabel>
166
+ <Input
167
+ value={name}
168
+ onChange={(e) => setName(e.target.value)}
169
+ placeholder="e.g. High equity opportunity"
170
+ />
171
+ </Field>
172
+
173
+ {/* Share across company */}
174
+ {isCompanyAdmin && (
175
+ <div className="flex items-center gap-2">
176
+ <Checkbox
177
+ id="alert-share"
178
+ checked={shareAcrossCompany}
179
+ onCheckedChange={(v) => setShareAcrossCompany(!!v)}
180
+ />
181
+ <Label htmlFor="alert-share" className="cursor-pointer font-normal">
182
+ Share across company
183
+ </Label>
184
+ </div>
185
+ )}
186
+
187
+ {/* Query builder */}
188
+ <div className="flex flex-col gap-1.5">
189
+ <p className="text-sm font-medium text-foreground">
190
+ Filter conditions
191
+ </p>
192
+ <ContactAlertQueryBuilder
193
+ value={tree}
194
+ onChange={setTree}
195
+ fields={ALERT_QUERY_FIELDS}
196
+ />
197
+ </div>
198
+
199
+ <DialogFooter>
200
+ <Button
201
+ variant="outline"
202
+ onClick={() => onOpenChange(false)}
203
+ disabled={isLoading}
204
+ >
205
+ Cancel
206
+ </Button>
207
+ <Button disabled={!canSave} onClick={handleSave}>
208
+ {mode === "edit" ? "Update Alert" : "Create Alert"}
209
+ </Button>
210
+ </DialogFooter>
211
+ </DialogContent>
212
+ </Dialog>
213
+ );
214
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ ContactAlertQueryBuilder,
3
+ ContactAlertDialog,
4
+ } from "./contact-alert-dialog";
5
+ export { ALERT_QUERY_FIELDS, createAlertTree } from "./config";
6
+ export { AlertSharingType } from "./types";
7
+ export type {
8
+ AlertQueryCombinator,
9
+ AlertQueryField,
10
+ AlertQueryFieldType,
11
+ AlertQueryOperator,
12
+ ContactAlertDialogProps,
13
+ ContactAlertQueryBuilderProps,
14
+ ContactAlertSeverity,
15
+ } from "./types";
@@ -0,0 +1,61 @@
1
+ import type { ImmutableTree, JsonGroup } from "@react-awesome-query-builder/ui";
2
+
3
+ export type AlertQueryCombinator = "AND" | "OR";
4
+
5
+ export type AlertQueryOperator =
6
+ | "equal"
7
+ | "not_equal"
8
+ | "less"
9
+ | "less_or_equal"
10
+ | "greater"
11
+ | "greater_or_equal"
12
+ | "between";
13
+
14
+ export type AlertQueryFieldType = "number" | "boolean";
15
+
16
+ export type ContactAlertSeverity = "INSIGHT" | "WATCH" | "NEED_ACTION";
17
+
18
+ export enum AlertSharingType {
19
+ PRIVATE = "PRIVATE",
20
+ COMPANY = "COMPANY",
21
+ }
22
+
23
+ export interface AlertQueryField {
24
+ key: string;
25
+ label: string;
26
+ type: AlertQueryFieldType;
27
+ /** "dollar" = prefix "$", "percent" = suffix "%" */
28
+ unit?: "dollar" | "percent";
29
+ /** Allowed operators (defaults to all numeric operators). */
30
+ operators?: AlertQueryOperator[];
31
+ }
32
+
33
+ export interface ContactAlertQueryBuilderProps {
34
+ /** Current ImmutableTree state — fully controlled. */
35
+ value: ImmutableTree;
36
+ onChange: (newTree: ImmutableTree) => void;
37
+ fields?: AlertQueryField[];
38
+ className?: string;
39
+ }
40
+
41
+ export interface ContactAlertDialogProps {
42
+ open: boolean;
43
+ onOpenChange: (open: boolean) => void;
44
+ mode?: "create" | "edit";
45
+ initialName?: string;
46
+ initialSeverity?: ContactAlertSeverity;
47
+ /** Initial query in JsonGroup format (array children1). Converted to ImmutableTree on open. */
48
+ initialQuery?: JsonGroup;
49
+ /** Show "Share across company" checkbox for company admins. */
50
+ isCompanyAdmin?: boolean;
51
+ initialShareAcrossCompany?: boolean;
52
+ onSave: (data: {
53
+ name: string;
54
+ severity: ContactAlertSeverity;
55
+ /** Backend-compatible ImmutableTree (filterSegment format). */
56
+ filterSegment: ImmutableTree;
57
+ sharingType: AlertSharingType;
58
+ }) => void;
59
+ isLoading?: boolean;
60
+ className?: string;
61
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ Utils as QbUtils,
3
+ type ImmutableTree,
4
+ } from "@react-awesome-query-builder/ui";
5
+ import type { AlertQueryField, AlertQueryOperator } from "./types";
6
+ import {
7
+ ALERT_QUERY_FIELDS,
8
+ ALL_NUMERIC_OPERATORS,
9
+ BOOLEAN_OPERATORS,
10
+ OPERATOR_LABELS,
11
+ } from "./config";
12
+
13
+ export function longestOf(labels: string[]): string {
14
+ return labels.reduce((a, b) => (a.length >= b.length ? a : b), "");
15
+ }
16
+
17
+ export function allOperatorLabels(fields: AlertQueryField[]): string[] {
18
+ const ops = new Set<AlertQueryOperator>();
19
+ for (const f of fields) {
20
+ const fieldOps =
21
+ f.type === "boolean"
22
+ ? BOOLEAN_OPERATORS
23
+ : (f.operators ?? ALL_NUMERIC_OPERATORS);
24
+ fieldOps.forEach((op) => ops.add(op));
25
+ }
26
+ return Array.from(ops).map((op) => OPERATOR_LABELS[op]);
27
+ }
28
+
29
+ export function formatWithCommas(raw: string): string {
30
+ if (!raw) return "";
31
+ const [int, dec] = raw.split(".");
32
+ const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
33
+ return dec !== undefined ? `${formatted}.${dec}` : formatted;
34
+ }
35
+
36
+ export function parseCommas(display: string): string {
37
+ return display.replace(/[^\d.]/g, "").replace(/(\..*)\./g, "$1");
38
+ }
39
+
40
+ export function ruleSummary(
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ ruleProps: any,
43
+ fields: AlertQueryField[],
44
+ ): string {
45
+ const field: string = ruleProps?.field ?? "";
46
+ const operator: string = ruleProps?.operator ?? "equal";
47
+ const value0 = ruleProps?.value?.[0];
48
+ const value1 = ruleProps?.value?.[1];
49
+
50
+ const fieldDef = fields.find((f) => f.key === field);
51
+ const fieldLabel = fieldDef?.label ?? field;
52
+ const opLabel = OPERATOR_LABELS[operator as AlertQueryOperator] ?? operator;
53
+
54
+ const formatVal = (v: unknown): string => {
55
+ if (v === null || v === undefined) return "…";
56
+ if (fieldDef?.type === "boolean") return v ? "Yes" : "No";
57
+ const n = typeof v === "number" ? v : parseFloat(String(v));
58
+ if (!isFinite(n)) return "…";
59
+ if (fieldDef?.unit === "dollar")
60
+ return `$${formatWithCommas(n.toString())}`;
61
+ if (fieldDef?.unit === "percent") return `${n}%`;
62
+ return String(v);
63
+ };
64
+
65
+ if (operator === "between") {
66
+ return `${fieldLabel} between ${formatVal(value0)} and ${formatVal(value1)}`;
67
+ }
68
+ return `${fieldLabel} ${opLabel} ${formatVal(value0)}`;
69
+ }
70
+
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ function checkGroupValid(children: any[]): boolean {
73
+ return children.some((child) => {
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ const c = child as any;
76
+ if (c.type === "rule") {
77
+ const field: string | undefined = c.properties?.field;
78
+ if (!field) return false;
79
+ const fieldDef = ALERT_QUERY_FIELDS.find((f) => f.key === field);
80
+ if (fieldDef?.type === "boolean") return true;
81
+ const v = c.properties?.value?.[0];
82
+ return v !== null && v !== undefined;
83
+ }
84
+ if (c.type === "group") return checkGroupValid(c.children1 ?? []);
85
+ return false;
86
+ });
87
+ }
88
+
89
+ export function isTreeValid(tree: ImmutableTree): boolean {
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ const json = QbUtils.getTree(tree) as any;
92
+ return checkGroupValid(json?.children1 ?? []);
93
+ }