hs-uix 2.1.0 → 2.2.0
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/README.md +3 -1
- package/common-components.d.ts +319 -68
- package/dist/calendar.js +397 -119
- package/dist/calendar.mjs +399 -119
- package/dist/common-components.js +3546 -88
- package/dist/common-components.mjs +3530 -84
- package/dist/datatable.js +108 -18
- package/dist/datatable.mjs +108 -18
- package/dist/experimental.js +2876 -0
- package/dist/experimental.mjs +2883 -0
- package/dist/feed.js +267 -38
- package/dist/feed.mjs +260 -37
- package/dist/filter.js +1379 -0
- package/dist/filter.mjs +1334 -0
- package/dist/form.js +222 -26
- package/dist/form.mjs +227 -27
- package/dist/index.js +3255 -353
- package/dist/index.mjs +3199 -344
- package/dist/kanban.js +282 -62
- package/dist/kanban.mjs +273 -61
- package/dist/safe.js +9207 -0
- package/dist/safe.mjs +9298 -0
- package/dist/utils.js +491 -75
- package/dist/utils.mjs +491 -75
- package/experimental.d.ts +1 -0
- package/filter.d.ts +1 -0
- package/index.d.ts +45 -3
- package/package.json +19 -1
- package/safe.d.ts +1 -0
- package/src/calendar/README.md +76 -5
- package/src/calendar/index.d.ts +108 -1
- package/src/common-components/README.md +140 -1
- package/src/datatable/README.md +0 -2
- package/src/experimental/README.md +126 -0
- package/src/experimental/index.d.ts +346 -0
- package/src/feed/README.md +69 -0
- package/src/feed/index.d.ts +103 -0
- package/src/filter/README.md +148 -0
- package/src/filter/index.d.ts +221 -0
- package/src/form/README.md +132 -4
- package/src/form/index.d.ts +82 -1
- package/src/kanban/README.md +119 -6
- package/src/kanban/index.d.ts +153 -2
- package/src/safe/README.md +108 -0
- package/src/safe/index.d.ts +158 -0
- package/src/utils/README.md +39 -0
- package/src/wizard/README.md +158 -0
- package/src/wizard/index.d.ts +138 -0
- package/utils.d.ts +17 -0
package/src/feed/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type FeedFilterType = "select" | "multiselect" | "dateRange";
|
|
|
7
7
|
export type FeedSortDirection = "asc" | "desc" | "ascending" | "descending";
|
|
8
8
|
export type FeedStatusVariant = "default" | "info" | "success" | "warning" | "danger";
|
|
9
9
|
export type FeedTagVariant = "default" | "info" | "success" | "warning" | "error";
|
|
10
|
+
export type FeedNewItemsBehavior = "immediate" | "pill";
|
|
10
11
|
|
|
11
12
|
export interface FeedOption<T = string | number | boolean> {
|
|
12
13
|
label: string;
|
|
@@ -74,6 +75,8 @@ export interface FeedItem {
|
|
|
74
75
|
avatarSize?: string | number;
|
|
75
76
|
icon?: ReactNode | string;
|
|
76
77
|
iconName?: string;
|
|
78
|
+
/** Icon color: native enum (`alert`/`warning`/`success`/`inherit`) or any CSS color (renders via the SVG fallback). */
|
|
79
|
+
iconColor?: string;
|
|
77
80
|
href?: string | { url: string; external?: boolean };
|
|
78
81
|
meta?: ReactNode | ReactNode[];
|
|
79
82
|
metadata?: ReactNode | ReactNode[];
|
|
@@ -141,6 +144,10 @@ export interface FeedLabels {
|
|
|
141
144
|
errorTitle?: string;
|
|
142
145
|
errorMessage?: string;
|
|
143
146
|
itemCount?: (shown: number, total: number, label: string) => string;
|
|
147
|
+
/** "Show N new items" pill label (string or count-aware function). */
|
|
148
|
+
newItems?: string | ((count: number) => string);
|
|
149
|
+
/** Text inside the info Tag marking recently arrived items. */
|
|
150
|
+
newItemTag?: string;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
export interface FeedTabOption<T = string | number | boolean> {
|
|
@@ -193,6 +200,80 @@ export interface FeedRecordLabel {
|
|
|
193
200
|
plural?: string;
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
export interface FeedTypePreset {
|
|
204
|
+
/** Native HubSpot icon name (verified against the native whitelist for the built-in presets). */
|
|
205
|
+
icon?: string;
|
|
206
|
+
/** Icon color: native enum (`alert`/`warning`/`success`/`inherit`) or any CSS color. */
|
|
207
|
+
color?: string;
|
|
208
|
+
/** Display label used as `typeLabel` when the item has none. */
|
|
209
|
+
label?: ReactNode;
|
|
210
|
+
/** StatusTag variant applied when the item has no status variant of its own. */
|
|
211
|
+
statusVariant?: FeedStatusVariant;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type FeedTypePresets = Record<string, FeedTypePreset>;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Built-in presets for HubSpot's standard activity types (`call`, `email`,
|
|
218
|
+
* `incoming_email`, `forwarded_email`, `meeting`, `note`, `task`, `sms`,
|
|
219
|
+
* `whatsapp`, `linkedin_message`, `postal_mail`, `conversation`).
|
|
220
|
+
*/
|
|
221
|
+
export declare const DEFAULT_FEED_TYPE_PRESETS: FeedTypePresets;
|
|
222
|
+
|
|
223
|
+
/** Resolve the preset for a type value (exact, lowercase, then snake_case lookup). */
|
|
224
|
+
export declare function lookupTypePreset(
|
|
225
|
+
type: unknown,
|
|
226
|
+
presets?: FeedTypePresets | null
|
|
227
|
+
): FeedTypePreset | null;
|
|
228
|
+
|
|
229
|
+
/** Merge a type preset UNDER an item (item-level values win). Returns the same reference when nothing changes. */
|
|
230
|
+
export declare function applyTypePreset<Row = FeedItem>(
|
|
231
|
+
item: Row,
|
|
232
|
+
typePresets?: FeedTypePresets | null
|
|
233
|
+
): Row;
|
|
234
|
+
|
|
235
|
+
export interface FeedPartitionOptions<Row = FeedItem> {
|
|
236
|
+
/** Ids already rendered — updates to these never buffer. */
|
|
237
|
+
knownIds?: Set<string | number> | null;
|
|
238
|
+
/** Id accessor; defaults to `item.id ?? item.key ?? index`. */
|
|
239
|
+
getId?: (item: Row, index: number) => string | number | undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface FeedPartitionResult<Row = FeedItem> {
|
|
243
|
+
visible: Row[];
|
|
244
|
+
buffered: Row[];
|
|
245
|
+
visibleIds: Array<string | number | undefined>;
|
|
246
|
+
bufferedIds: Array<string | number | undefined>;
|
|
247
|
+
/** New monotonic watermark over VISIBLE items only (epoch ms), or null. */
|
|
248
|
+
newestTs: number | null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Pure live-buffer kernel: split items into visible vs newer-than-watermark buffered. */
|
|
252
|
+
export declare function partitionNewItems<Row = FeedItem>(
|
|
253
|
+
prevNewestTs: number | null | undefined,
|
|
254
|
+
items: Row[] | null | undefined,
|
|
255
|
+
getTs: (item: Row) => unknown,
|
|
256
|
+
options?: FeedPartitionOptions<Row>
|
|
257
|
+
): FeedPartitionResult<Row>;
|
|
258
|
+
|
|
259
|
+
export interface FeedFlushResult<Row = FeedItem> {
|
|
260
|
+
/** Buffered items first, then the previously visible items. */
|
|
261
|
+
items: Row[];
|
|
262
|
+
flushed: Row[];
|
|
263
|
+
/** New watermark across ALL merged items (epoch ms), or null. */
|
|
264
|
+
newestTs: number | null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Pure flush: merge the buffer into the visible list and compute the new watermark. */
|
|
268
|
+
export declare function flushBuffer<Row = FeedItem>(
|
|
269
|
+
visible: Row[] | null | undefined,
|
|
270
|
+
buffered: Row[] | null | undefined,
|
|
271
|
+
getTs: (item: Row) => unknown
|
|
272
|
+
): FeedFlushResult<Row>;
|
|
273
|
+
|
|
274
|
+
/** Coerce Date | epoch number | parseable string | { year, month, date } to epoch ms (or null). */
|
|
275
|
+
export declare function toTimestampMs(value: unknown): number | null;
|
|
276
|
+
|
|
196
277
|
export interface FeedProps<Row = FeedItem> {
|
|
197
278
|
items?: Row[];
|
|
198
279
|
fields?: FeedField<Row>[];
|
|
@@ -274,6 +355,28 @@ export interface FeedProps<Row = FeedItem> {
|
|
|
274
355
|
collapsedIds?: (string | number)[];
|
|
275
356
|
onCollapsedIdsChange?: (next: (string | number)[]) => void;
|
|
276
357
|
showCollapseToggle?: boolean;
|
|
358
|
+
/**
|
|
359
|
+
* Real-time append behavior for items that arrive NEWER than the
|
|
360
|
+
* previously-newest visible timestamp. "immediate" (default) renders them
|
|
361
|
+
* right away; "pill" holds them in a buffer behind a centered
|
|
362
|
+
* "Show N new items" Button until clicked. Updates to already-visible items
|
|
363
|
+
* (matched by key) are never buffered.
|
|
364
|
+
*/
|
|
365
|
+
newItemsBehavior?: FeedNewItemsBehavior;
|
|
366
|
+
/** Called with the released items when the "Show N new items" pill is clicked. */
|
|
367
|
+
onNewItemsFlush?: (items: Row[]) => void;
|
|
368
|
+
/**
|
|
369
|
+
* Window in ms during which flushed/freshly-prepended items carry an info
|
|
370
|
+
* Tag "New" marker. `false` (default) disables the marker. The initial load
|
|
371
|
+
* is never marked.
|
|
372
|
+
*/
|
|
373
|
+
highlightNew?: number | false;
|
|
374
|
+
/**
|
|
375
|
+
* Per-type display defaults merged UNDER item values (item wins):
|
|
376
|
+
* `{ [type]: { icon, color, label, statusVariant } }`. Pass `true` to use
|
|
377
|
+
* DEFAULT_FEED_TYPE_PRESETS. Lookup by `item.type` is case-insensitive.
|
|
378
|
+
*/
|
|
379
|
+
typePresets?: FeedTypePresets | boolean | null;
|
|
277
380
|
}
|
|
278
381
|
|
|
279
382
|
export declare function Feed<Row = FeedItem>(props: FeedProps<Row>): ReactNode;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# FilterBuilder (hs-uix/filter)
|
|
2
|
+
|
|
3
|
+
The HubSpot list/workflow segment-builder pattern as one component: nested AND/OR groups of property → operator → value rows. If your extension needs "show me deals where amount > 10k AND (stage is X OR stage is Y)", this is the component — stop hand-rolling three Selects in a Flex row with an ad-hoc state shape. The tree it emits converts directly to HubSpot CRM search `filterGroups` via `toCrmSearchFilterGroups`.
|
|
4
|
+
|
|
5
|
+
Renders entirely with native components: `Select` / `MultiSelect` / `Input` / `NumberInput` / `DateInput` rows inside `Flex`, nested groups in `Tile`. Every action is a `Button` carrying HubSpot's segment-builder iconography — add (`+`) for "Add filter" / "Add filter group", a remove (`x`) icon button on each condition row, and copy / trash icon buttons on each group header for clone / delete.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { FilterBuilder, toCrmSearchFilterGroups, validateTree } from "hs-uix/filter";
|
|
12
|
+
|
|
13
|
+
const PROPERTIES = [
|
|
14
|
+
{ name: "dealname", label: "Deal name", type: "string" },
|
|
15
|
+
{ name: "amount", label: "Amount", type: "number" },
|
|
16
|
+
{ name: "closedate", label: "Close date", type: "date" },
|
|
17
|
+
{
|
|
18
|
+
name: "dealstage",
|
|
19
|
+
label: "Stage",
|
|
20
|
+
type: "enum",
|
|
21
|
+
options: [
|
|
22
|
+
{ label: "Appointment", value: "appointmentscheduled" },
|
|
23
|
+
{ label: "Qualified", value: "qualifiedtobuy" },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{ name: "hs_is_closed", label: "Is closed", type: "bool" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const SegmentEditor = () => {
|
|
30
|
+
const [tree, setTree] = useState();
|
|
31
|
+
|
|
32
|
+
const runSearch = () => {
|
|
33
|
+
if (!tree || !validateTree(tree, PROPERTIES).valid) return;
|
|
34
|
+
const { filterGroups } = toCrmSearchFilterGroups(tree);
|
|
35
|
+
// → hubspot.fetch CRM search body: { filterGroups, properties: [...], ... }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return <FilterBuilder properties={PROPERTIES} defaultValue={tree} onChange={setTree} />;
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The filter tree (public contract)
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
{
|
|
46
|
+
type: "group",
|
|
47
|
+
operator: "AND" | "OR",
|
|
48
|
+
filters: [
|
|
49
|
+
{ type: "condition", property: "amount", operator: "BETWEEN", value: 1000, highValue: 5000 },
|
|
50
|
+
{ type: "group", operator: "OR", filters: [ /* nested */ ] },
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- The root is always a group. An empty root means "no filters".
|
|
56
|
+
- Conditions carry `value` (scalar, or array for `IN` / `NOT_IN`) and `highValue` (only `BETWEEN`). `HAS_PROPERTY` / `NOT_HAS_PROPERTY` carry no value.
|
|
57
|
+
- Paths into the tree are index arrays: `[]` is the root, `[1, 0]` is the root's second child's first child.
|
|
58
|
+
|
|
59
|
+
## Operators per property type (CRM search names)
|
|
60
|
+
|
|
61
|
+
| Type | Operators | Value editor |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `string` | `EQ`, `NEQ`, `CONTAINS_TOKEN`, `NOT_CONTAINS_TOKEN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY` | `Input` |
|
|
64
|
+
| `number` | `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`, `BETWEEN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY` | `NumberInput` (× 2 for `BETWEEN`) |
|
|
65
|
+
| `date` / `datetime` | same as `number`, labeled "is after" / "is before" | `DateInput` (× 2 for `BETWEEN`) |
|
|
66
|
+
| `enum` | `IN`, `NOT_IN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY` | `MultiSelect` over the property's `options` |
|
|
67
|
+
| `bool` | `EQ` | `Select` (True / False → `"true"` / `"false"`) |
|
|
68
|
+
|
|
69
|
+
`HAS_PROPERTY` / `NOT_HAS_PROPERTY` render no value editor. The native `DateInput` is date-only, so `datetime` properties get day precision in the UI.
|
|
70
|
+
|
|
71
|
+
## Features
|
|
72
|
+
|
|
73
|
+
- Controlled (`value` + `onChange`) or uncontrolled (`defaultValue`) tree state
|
|
74
|
+
- Nested groups to `maxDepth` (default 2 — root plus one level, matching HubSpot's builder)
|
|
75
|
+
- Per-group AND/OR toggle rendered between rows; changing any separator updates the whole group
|
|
76
|
+
- Nested groups get a numbered heading ("Group 1", "Group 2", …) with clone (copy icon) and delete (trash icon) buttons, matching HubSpot's builder
|
|
77
|
+
- Property changes keep the operator when still valid for the new type, otherwise reset it; values are always cleared
|
|
78
|
+
- Operator changes keep values whose shape still fits (scalar→scalar, `IN`↔`NOT_IN`)
|
|
79
|
+
- Removing a group's last row prunes the now-empty group (root always survives)
|
|
80
|
+
- `readOnly` mode renders the tree without add/remove/edit affordances
|
|
81
|
+
- Every behavioral rule lives in pure, exported, unit-tested helpers (`filterTree.js`)
|
|
82
|
+
|
|
83
|
+
## `toCrmSearchFilterGroups(tree, options?)`
|
|
84
|
+
|
|
85
|
+
Converts the tree to the HubSpot CRM search shape — `{ filterGroups: [{ filters: [...] }] }`, where filters in a group are ANDed and the groups are ORed.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
toCrmSearchFilterGroups({
|
|
89
|
+
type: "group", operator: "AND", filters: [
|
|
90
|
+
{ type: "condition", property: "amount", operator: "GT", value: 1000 },
|
|
91
|
+
{ type: "group", operator: "OR", filters: [
|
|
92
|
+
{ type: "condition", property: "dealstage", operator: "IN", value: ["a"] },
|
|
93
|
+
{ type: "condition", property: "hs_is_closed", operator: "EQ", value: "false" },
|
|
94
|
+
] },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
// → { filterGroups: [
|
|
98
|
+
// { filters: [{ propertyName: "amount", operator: "GT", value: 1000 }, { propertyName: "dealstage", operator: "IN", values: ["a"] }] },
|
|
99
|
+
// { filters: [{ propertyName: "amount", operator: "GT", value: 1000 }, { propertyName: "hs_is_closed", operator: "EQ", value: "false" }] },
|
|
100
|
+
// ] }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Flattening rules.** The tree is expanded to disjunctive normal form, so nesting at ANY depth converts — `A AND (B OR C)` distributes to `[A,B] | [A,C]`, ORs nested under ORs flatten, and `(A OR B) AND (C OR D)` becomes the 4-group cartesian product. The cost is combinatorial: every OR nested under an AND **multiplies** filterGroups and duplicates the sibling conditions into each one.
|
|
104
|
+
|
|
105
|
+
**Limits.** CRM search rejects more than 5 filterGroups, 6 filters per group, or 18 filters total. When the expansion exceeds a limit, this **throws** with a descriptive message instead of sending a request that will 400. Tune via `maxGroups` / `maxFiltersPerGroup` / `maxTotalFilters`, or pass `enforceLimits: false`.
|
|
106
|
+
|
|
107
|
+
**Value coercion** (default on, disable with `coerceValues: false`): `DateInput` value objects (`{ year, month, date }`) become epoch-ms numbers (local midnight, matching the `hs-uix/utils` dateRange helpers); booleans become `"true"` / `"false"` strings. Pre-convert and pass numbers/strings yourself if you need different semantics (e.g. UTC midnight).
|
|
108
|
+
|
|
109
|
+
Empty nested groups throw — run `validateTree` first and gate your search button on `valid`.
|
|
110
|
+
|
|
111
|
+
## Props
|
|
112
|
+
|
|
113
|
+
### `<FilterBuilder />`
|
|
114
|
+
|
|
115
|
+
| Prop | Type | Default | Notes |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `properties` | `FilterProperty[]` | — | Required. `{ name, label?, type?, options? }`; `type` defaults to `"string"`; `enum` needs `options`. |
|
|
118
|
+
| `value` | `FilterGroupNode` | — | Controlled tree. |
|
|
119
|
+
| `defaultValue` | `FilterGroupNode` | empty AND group | Initial tree for uncontrolled mode. |
|
|
120
|
+
| `onChange` | `(tree) => void` | — | Fired with the full new tree after every edit. |
|
|
121
|
+
| `maxDepth` | `number` | `2` | Max group nesting; root counts as 1. `1` disables "Add filter group". |
|
|
122
|
+
| `labels` | `FilterBuilderLabels` | built-in copy | Overrides for `addFilter`, `addGroup`, `remove`, `removeGroup`, `cloneGroup`, `group`, `and`, `or`, `property`, `operator`, `value`, `values`, `between`, `empty`, `true`, `false`. `remove` / `removeGroup` / `cloneGroup` are screen-reader text on the icon buttons; `group` prefixes group headings. |
|
|
123
|
+
| `operatorLabels` | `Record<operator, string>` | — | Per-operator label overrides for the operator dropdowns. |
|
|
124
|
+
| `readOnly` | `boolean` | `false` | Render without edit affordances. |
|
|
125
|
+
| `namePrefix` | `string` | `"filter-builder"` | Prefix for native input `name`s; set when rendering two builders on one surface. |
|
|
126
|
+
| `...rest` | — | — | Spread onto the root `Flex`. |
|
|
127
|
+
|
|
128
|
+
### Pure helpers (all exported from `hs-uix/filter`)
|
|
129
|
+
|
|
130
|
+
| Export | Signature | Notes |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `FILTER_OPERATORS` | `Record<type, operator[]>` | The operator sets per property type. |
|
|
133
|
+
| `getOperatorOptions` | `(type, labelOverrides?) => { label, value }[]` | Select-ready operator options; date types get before/after phrasing. |
|
|
134
|
+
| `operatorExpectsValue` / `operatorExpectsHighValue` / `operatorExpectsValues` | `(operator) => boolean` | Value-arity rules. |
|
|
135
|
+
| `createCondition` | `(property?, operator?, value?, highValue?) => node` | `value`/`highValue` keys only present when provided. |
|
|
136
|
+
| `createGroup` | `(operator?, filters?) => node` | Defaults to an empty AND group. |
|
|
137
|
+
| `isGroupNode` / `isConditionNode` | `(node) => boolean` | Type guards. |
|
|
138
|
+
| `getNodeAtPath` | `(tree, path) => node \| undefined` | Lenient read. |
|
|
139
|
+
| `addFilter` | `(tree, groupPath, node) => tree` | Immutable append; throws if the path isn't a group. |
|
|
140
|
+
| `updateFilter` | `(tree, path, patch \| fn) => tree` | Object patch merges; function patch replaces the node. |
|
|
141
|
+
| `removeFilter` | `(tree, path, { pruneEmptyGroups? }) => tree` | Throws on the root path; prune cascades upward but never removes the root. |
|
|
142
|
+
| `duplicateFilter` | `(tree, path) => tree` | Deep-clones the node at `path` (condition or group) and inserts the copy right after it. Throws on the root path. |
|
|
143
|
+
| `countConditions` | `(node) => number` | Conditions only; groups don't count. |
|
|
144
|
+
| `changeConditionProperty` | `(condition, propertyDef) => condition` | Keeps a still-valid operator, clears values. |
|
|
145
|
+
| `changeConditionOperator` | `(condition, operator) => condition` | Keeps shape-compatible values. |
|
|
146
|
+
| `validateTree` | `(tree, properties?) => { valid, errors: [{ path, message }] }` | Empty root is valid; empty nested groups are not. |
|
|
147
|
+
| `conditionToCrmFilter` | `(condition, { coerceValues? }) => crmFilter` | One condition → `{ propertyName, operator, value/values/highValue }`. |
|
|
148
|
+
| `toCrmSearchFilterGroups` | `(tree, options?) => { filterGroups }` | DNF expansion + limit enforcement (see above). |
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type FilterPropertyType = "string" | "number" | "date" | "datetime" | "enum" | "bool";
|
|
4
|
+
|
|
5
|
+
export type FilterOperator =
|
|
6
|
+
| "EQ"
|
|
7
|
+
| "NEQ"
|
|
8
|
+
| "CONTAINS_TOKEN"
|
|
9
|
+
| "NOT_CONTAINS_TOKEN"
|
|
10
|
+
| "GT"
|
|
11
|
+
| "GTE"
|
|
12
|
+
| "LT"
|
|
13
|
+
| "LTE"
|
|
14
|
+
| "BETWEEN"
|
|
15
|
+
| "IN"
|
|
16
|
+
| "NOT_IN"
|
|
17
|
+
| "HAS_PROPERTY"
|
|
18
|
+
| "NOT_HAS_PROPERTY";
|
|
19
|
+
|
|
20
|
+
export type FilterGroupOperator = "AND" | "OR";
|
|
21
|
+
|
|
22
|
+
/** Path into nested `filters` arrays. `[]` is the root group, `[1, 0]` is the root's second child's first child. */
|
|
23
|
+
export type FilterTreePath = number[];
|
|
24
|
+
|
|
25
|
+
export interface FilterPropertyOption {
|
|
26
|
+
label: string;
|
|
27
|
+
value: string | number | boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FilterProperty {
|
|
31
|
+
name: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
/** Defaults to "string". Enum properties need `options`. */
|
|
34
|
+
type?: FilterPropertyType;
|
|
35
|
+
options?: FilterPropertyOption[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FilterConditionNode {
|
|
39
|
+
type: "condition";
|
|
40
|
+
property: string;
|
|
41
|
+
/** "" while the user has not picked an operator yet. */
|
|
42
|
+
operator: FilterOperator | "";
|
|
43
|
+
/** Scalar for most operators; array for IN / NOT_IN; absent for HAS_PROPERTY / NOT_HAS_PROPERTY. */
|
|
44
|
+
value?: unknown;
|
|
45
|
+
/** Upper bound, BETWEEN only. */
|
|
46
|
+
highValue?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FilterGroupNode {
|
|
50
|
+
type: "group";
|
|
51
|
+
operator: FilterGroupOperator;
|
|
52
|
+
filters: FilterNode[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type FilterNode = FilterConditionNode | FilterGroupNode;
|
|
56
|
+
|
|
57
|
+
export interface FilterBuilderLabels {
|
|
58
|
+
addFilter?: string;
|
|
59
|
+
addGroup?: string;
|
|
60
|
+
/** Screen-reader text on the condition row's remove (x) icon button. */
|
|
61
|
+
remove?: string;
|
|
62
|
+
/** Screen-reader text on the group header's delete (trash) icon button. */
|
|
63
|
+
removeGroup?: string;
|
|
64
|
+
/** Screen-reader text on the group header's clone (copy) icon button. */
|
|
65
|
+
cloneGroup?: string;
|
|
66
|
+
/** Group heading prefix — rendered as "Group 1", "Group 2", … */
|
|
67
|
+
group?: string;
|
|
68
|
+
and?: string;
|
|
69
|
+
or?: string;
|
|
70
|
+
property?: string;
|
|
71
|
+
operator?: string;
|
|
72
|
+
value?: string;
|
|
73
|
+
values?: string;
|
|
74
|
+
between?: string;
|
|
75
|
+
empty?: string;
|
|
76
|
+
true?: string;
|
|
77
|
+
false?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FilterValidationError {
|
|
81
|
+
path: FilterTreePath;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface FilterValidationResult {
|
|
86
|
+
valid: boolean;
|
|
87
|
+
errors: FilterValidationError[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface FilterCrmSearchFilter {
|
|
91
|
+
propertyName: string;
|
|
92
|
+
operator: FilterOperator;
|
|
93
|
+
value?: string | number | boolean;
|
|
94
|
+
highValue?: string | number | boolean;
|
|
95
|
+
values?: Array<string | number | boolean>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface FilterCrmSearchFilterGroups {
|
|
99
|
+
filterGroups: Array<{ filters: FilterCrmSearchFilter[] }>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FilterToCrmSearchOptions {
|
|
103
|
+
/** Max filterGroups after DNF expansion (default 5, the CRM search limit). */
|
|
104
|
+
maxGroups?: number;
|
|
105
|
+
/** Max filters per filterGroup (default 6). */
|
|
106
|
+
maxFiltersPerGroup?: number;
|
|
107
|
+
/** Max filters across all groups (default 18). */
|
|
108
|
+
maxTotalFilters?: number;
|
|
109
|
+
/** Set false to skip limit checks (default true). */
|
|
110
|
+
enforceLimits?: boolean;
|
|
111
|
+
/** Set false to pass values through without DateInput→epoch-ms / boolean→string coercion (default true). */
|
|
112
|
+
coerceValues?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface FilterConditionToCrmOptions {
|
|
116
|
+
coerceValues?: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface FilterRemoveOptions {
|
|
120
|
+
/** Also remove ancestor groups left empty by the removal (root is never pruned). */
|
|
121
|
+
pruneEmptyGroups?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface FilterBuilderProps {
|
|
125
|
+
properties: FilterProperty[];
|
|
126
|
+
/** Controlled tree value. */
|
|
127
|
+
value?: FilterGroupNode | null;
|
|
128
|
+
/** Initial tree for uncontrolled mode. */
|
|
129
|
+
defaultValue?: FilterGroupNode | null;
|
|
130
|
+
onChange?: (tree: FilterGroupNode) => void;
|
|
131
|
+
/** Max group nesting depth; root counts as 1 (default 2). */
|
|
132
|
+
maxDepth?: number;
|
|
133
|
+
labels?: FilterBuilderLabels;
|
|
134
|
+
/** Per-operator label overrides for the operator dropdowns. */
|
|
135
|
+
operatorLabels?: Partial<Record<FilterOperator, string>>;
|
|
136
|
+
readOnly?: boolean;
|
|
137
|
+
/** Prefix for native input `name`s; set when rendering two builders on one surface. */
|
|
138
|
+
namePrefix?: string;
|
|
139
|
+
/** Remaining props are spread onto the root Flex. */
|
|
140
|
+
[key: string]: unknown;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export declare const FILTER_OPERATORS: Record<FilterPropertyType, FilterOperator[]>;
|
|
144
|
+
|
|
145
|
+
export declare function getOperatorOptions(
|
|
146
|
+
type?: FilterPropertyType | string,
|
|
147
|
+
labelOverrides?: Partial<Record<FilterOperator, string>>
|
|
148
|
+
): Array<{ label: string; value: FilterOperator }>;
|
|
149
|
+
|
|
150
|
+
export declare function operatorExpectsValue(operator?: string): boolean;
|
|
151
|
+
export declare function operatorExpectsHighValue(operator?: string): boolean;
|
|
152
|
+
export declare function operatorExpectsValues(operator?: string): boolean;
|
|
153
|
+
|
|
154
|
+
export declare function isGroupNode(node: unknown): node is FilterGroupNode;
|
|
155
|
+
export declare function isConditionNode(node: unknown): node is FilterConditionNode;
|
|
156
|
+
|
|
157
|
+
export declare function createCondition(
|
|
158
|
+
property?: string,
|
|
159
|
+
operator?: FilterOperator | "",
|
|
160
|
+
value?: unknown,
|
|
161
|
+
highValue?: unknown
|
|
162
|
+
): FilterConditionNode;
|
|
163
|
+
|
|
164
|
+
export declare function createGroup(
|
|
165
|
+
operator?: FilterGroupOperator,
|
|
166
|
+
filters?: FilterNode[]
|
|
167
|
+
): FilterGroupNode;
|
|
168
|
+
|
|
169
|
+
export declare function getNodeAtPath(tree: FilterNode, path: FilterTreePath): FilterNode | undefined;
|
|
170
|
+
|
|
171
|
+
export declare function addFilter(
|
|
172
|
+
tree: FilterGroupNode,
|
|
173
|
+
path: FilterTreePath,
|
|
174
|
+
node: FilterNode
|
|
175
|
+
): FilterGroupNode;
|
|
176
|
+
|
|
177
|
+
export declare function updateFilter(
|
|
178
|
+
tree: FilterGroupNode,
|
|
179
|
+
path: FilterTreePath,
|
|
180
|
+
patch: Partial<FilterConditionNode> | Partial<FilterGroupNode> | ((node: FilterNode) => FilterNode)
|
|
181
|
+
): FilterGroupNode;
|
|
182
|
+
|
|
183
|
+
export declare function removeFilter(
|
|
184
|
+
tree: FilterGroupNode,
|
|
185
|
+
path: FilterTreePath,
|
|
186
|
+
options?: FilterRemoveOptions
|
|
187
|
+
): FilterGroupNode;
|
|
188
|
+
|
|
189
|
+
export declare function duplicateFilter(
|
|
190
|
+
tree: FilterGroupNode,
|
|
191
|
+
path: FilterTreePath
|
|
192
|
+
): FilterGroupNode;
|
|
193
|
+
|
|
194
|
+
export declare function countConditions(node: FilterNode): number;
|
|
195
|
+
|
|
196
|
+
export declare function changeConditionProperty(
|
|
197
|
+
condition: FilterConditionNode,
|
|
198
|
+
property: FilterProperty | string
|
|
199
|
+
): FilterConditionNode;
|
|
200
|
+
|
|
201
|
+
export declare function changeConditionOperator(
|
|
202
|
+
condition: FilterConditionNode,
|
|
203
|
+
operator: FilterOperator | ""
|
|
204
|
+
): FilterConditionNode;
|
|
205
|
+
|
|
206
|
+
export declare function validateTree(
|
|
207
|
+
tree: FilterNode,
|
|
208
|
+
properties?: FilterProperty[]
|
|
209
|
+
): FilterValidationResult;
|
|
210
|
+
|
|
211
|
+
export declare function conditionToCrmFilter(
|
|
212
|
+
condition: FilterConditionNode,
|
|
213
|
+
options?: FilterConditionToCrmOptions
|
|
214
|
+
): FilterCrmSearchFilter;
|
|
215
|
+
|
|
216
|
+
export declare function toCrmSearchFilterGroups(
|
|
217
|
+
tree: FilterGroupNode,
|
|
218
|
+
options?: FilterToCrmSearchOptions
|
|
219
|
+
): FilterCrmSearchFilterGroups;
|
|
220
|
+
|
|
221
|
+
export declare function FilterBuilder(props: FilterBuilderProps): ReactNode;
|