hs-uix 2.0.0 → 2.1.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 +54 -31
- package/calendar.d.ts +1 -0
- package/common-components.d.ts +143 -0
- package/datatable.d.ts +1 -27
- package/dist/calendar.js +2003 -0
- package/dist/calendar.mjs +2004 -0
- package/dist/common-components.js +1072 -717
- package/dist/common-components.mjs +1344 -994
- package/dist/datatable.js +1114 -251
- package/dist/datatable.mjs +1089 -219
- package/dist/feed.js +1166 -154
- package/dist/feed.mjs +1170 -153
- package/dist/form.js +792 -166
- package/dist/form.mjs +693 -68
- package/dist/index.js +8354 -7077
- package/dist/index.mjs +8425 -7128
- package/dist/kanban.js +1076 -329
- package/dist/kanban.mjs +1061 -311
- package/dist/utils.js +1492 -646
- package/dist/utils.mjs +1410 -573
- package/feed.d.ts +1 -1
- package/form.d.ts +1 -28
- package/index.d.ts +54 -103
- package/kanban.d.ts +1 -1
- package/package.json +10 -6
- package/src/calendar/README.md +301 -0
- package/src/calendar/index.d.ts +201 -0
- package/src/common-components/README.md +400 -0
- package/{packages → src}/datatable/README.md +10 -10
- package/{packages → src}/datatable/index.d.ts +11 -0
- package/{packages → src}/feed/README.md +3 -1
- package/{packages → src}/feed/index.d.ts +18 -0
- package/{packages → src}/form/README.md +24 -10
- package/{packages → src}/kanban/README.md +13 -13
- package/{packages → src}/kanban/index.d.ts +18 -7
- package/src/utils/README.md +511 -0
- package/utils.d.ts +55 -3
- /package/{packages → src}/form/index.d.ts +0 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# utils
|
|
2
|
+
|
|
3
|
+
Pure helper functions for formatting, mapping, guards, and lightweight data transformations.
|
|
4
|
+
|
|
5
|
+
## Current modules
|
|
6
|
+
|
|
7
|
+
- `formatters.js` — number / currency / date / percentage formatters (locale-aware, `Intl`-based)
|
|
8
|
+
- `options.js` — build `{ label, value }` option arrays from raw records; resolve labels from values
|
|
9
|
+
- `hubspotValues.js` — type guards for HubSpot's `DateInput` / `TimeInput` / `DateTimeInput` value shapes
|
|
10
|
+
- `collections.js` — tiny array helpers (`sumBy`)
|
|
11
|
+
- `tagVariants.js` — map free-form status strings to semantic tag variants, plus sort comparators keyed by variant
|
|
12
|
+
- `viewAdapters.js` — shape transforms between DataTable columns and Kanban cardFields (power a single "same data, different view" toggle)
|
|
13
|
+
- `query.js` — shared collection query helpers: empty filter values, filter reset, active-filter chips, filtering, and search
|
|
14
|
+
- `crmSearchAdapters.js` — CRM-bound data components (`CrmDataTable`, `CrmKanban`) plus the lower-level CRM search hooks and config builders behind them
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
|
|
18
|
+
This folder is for non-visual logic only.
|
|
19
|
+
|
|
20
|
+
Use `utils` when the export is a pure function that helps format values, build config, validate HubSpot-shaped objects, or transform data for display.
|
|
21
|
+
|
|
22
|
+
## Import path
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import {
|
|
26
|
+
// formatters
|
|
27
|
+
formatCurrency,
|
|
28
|
+
formatCurrencyCompact,
|
|
29
|
+
formatDate,
|
|
30
|
+
formatDateTime,
|
|
31
|
+
formatPercentage,
|
|
32
|
+
// options
|
|
33
|
+
buildOptions,
|
|
34
|
+
findOptionLabel,
|
|
35
|
+
// hubspotValues
|
|
36
|
+
isDateValueObject,
|
|
37
|
+
isTimeValueObject,
|
|
38
|
+
isDateTimeValueObject,
|
|
39
|
+
// collections
|
|
40
|
+
sumBy,
|
|
41
|
+
// tagVariants
|
|
42
|
+
getAutoTagVariant,
|
|
43
|
+
getAutoStatusTagVariant,
|
|
44
|
+
getAutoTagDisplayValue,
|
|
45
|
+
createStatusTagSortComparator,
|
|
46
|
+
// viewAdapters
|
|
47
|
+
deriveCardFieldsFromColumns,
|
|
48
|
+
// query
|
|
49
|
+
buildActiveFilterChips,
|
|
50
|
+
resetFilterValues,
|
|
51
|
+
getEmptyFilterValues,
|
|
52
|
+
filterRows,
|
|
53
|
+
searchRows,
|
|
54
|
+
// crmSearchAdapters
|
|
55
|
+
CrmDataTable,
|
|
56
|
+
CrmKanban,
|
|
57
|
+
useCrmSearchDataSource,
|
|
58
|
+
useCrmSearchOptions,
|
|
59
|
+
makeCrmSearchSelectField,
|
|
60
|
+
makeCrmSearchMultiSelectField,
|
|
61
|
+
} from "hs-uix/utils";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## formatters.js
|
|
67
|
+
|
|
68
|
+
All formatters are pure `Intl.NumberFormat` / `toLocaleString` wrappers. Every option accepts a trailing `options` object that spreads into the underlying `Intl` call, so anything `Intl.NumberFormat` supports (narrow symbol, specific fraction digits, grouping) is reachable.
|
|
69
|
+
|
|
70
|
+
Defaults: `locale = "en-US"`, `currency = "USD"`. All formatters treat `null` / `undefined` as `0` or `""` so they're safe to use on partially-loaded data.
|
|
71
|
+
|
|
72
|
+
### `formatCurrency(value, opts?)`
|
|
73
|
+
|
|
74
|
+
Standard currency with no fractional digits by default.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
formatCurrency(1234.56) // → "$1,235"
|
|
78
|
+
formatCurrency(1234.56, { maximumFractionDigits: 2 }) // → "$1,234.56"
|
|
79
|
+
formatCurrency(9500, { currency: "EUR" }) // → "€9,500"
|
|
80
|
+
formatCurrency(null) // → "$0"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Option | Default | Notes |
|
|
84
|
+
| ------ | ------- | ----- |
|
|
85
|
+
| `locale` | `"en-US"` | Any `Intl` locale tag |
|
|
86
|
+
| `currency` | `"USD"` | ISO 4217 code |
|
|
87
|
+
| `maximumFractionDigits` | `0` | Set to `2` for cents |
|
|
88
|
+
| _any `Intl.NumberFormat` option_ | — | Spreads through |
|
|
89
|
+
|
|
90
|
+
### `formatCurrencyCompact(value, opts?)`
|
|
91
|
+
|
|
92
|
+
Same as `formatCurrency` but uses compact notation — the `$123.58M / $4.16K / $32` shorthand HubSpot uses for headline numbers (deal totals, pipeline value). Good for metric panels where the raw figure would dominate.
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
formatCurrencyCompact(123_580_000) // → "$123.6M"
|
|
96
|
+
formatCurrencyCompact(4160) // → "$4.2K"
|
|
97
|
+
formatCurrencyCompact(32) // → "$32"
|
|
98
|
+
formatCurrencyCompact(12_000, { compactDisplay: "long" }) // → "$12 thousand"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Option | Default | Notes |
|
|
102
|
+
| ------ | ------- | ----- |
|
|
103
|
+
| `locale`, `currency` | (as above) | — |
|
|
104
|
+
| `maximumFractionDigits` | `1` | One fractional digit reads cleanly across magnitudes |
|
|
105
|
+
| `compactDisplay` | `"short"` | `"short"` → M / K, `"long"` → million / thousand |
|
|
106
|
+
|
|
107
|
+
### `formatDate(value, opts?)`
|
|
108
|
+
|
|
109
|
+
Accepts a `Date`, ISO string, or timestamp; returns a formatted date string. Invalid/null input returns `""`.
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
formatDate(new Date(2026, 3, 15)) // → "Apr 15, 2026"
|
|
113
|
+
formatDate("2026-04-15") // → "Apr 15, 2026"
|
|
114
|
+
formatDate(Date.now(), { month: "numeric" }) // → "4/15/2026"
|
|
115
|
+
formatDate(null) // → ""
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Defaults: `month: "short"`, `day: "numeric"`, `year: "numeric"`.
|
|
119
|
+
|
|
120
|
+
### `formatDateTime(value, opts?)`
|
|
121
|
+
|
|
122
|
+
Same as `formatDate` but includes time of day.
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
formatDateTime(new Date(2026, 3, 15, 14, 30)) // → "Apr 15, 2026, 2:30 PM"
|
|
126
|
+
formatDateTime("2026-04-15T14:30:00Z") // → "Apr 15, 2026, 9:30 AM" (local)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Defaults add `hour: "numeric"`, `minute: "2-digit"` to the date options.
|
|
130
|
+
|
|
131
|
+
### `formatPercentage(value, opts?)`
|
|
132
|
+
|
|
133
|
+
Takes a **ratio** (0.15 = 15%), not a percentage number.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
formatPercentage(0.15) // → "15%"
|
|
137
|
+
formatPercentage(0.1567, { maximumFractionDigits: 1 }) // → "15.6%"
|
|
138
|
+
formatPercentage(1) // → "100%"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## options.js
|
|
144
|
+
|
|
145
|
+
### `buildOptions(items, opts?)`
|
|
146
|
+
|
|
147
|
+
Map a raw array into the `{ label, value }` shape every HubSpot `Select` / `MultiSelect` expects. Supports custom key names, map functions, and optional `description` passthrough.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
buildOptions(["Draft", "Published"])
|
|
151
|
+
// → [{ label: "Draft", value: "Draft" }, { label: "Published", value: "Published" }]
|
|
152
|
+
|
|
153
|
+
buildOptions(
|
|
154
|
+
[{ name: "Acme", id: 1 }, { name: "Globex", id: 2 }],
|
|
155
|
+
{ labelKey: "name", valueKey: "id" }
|
|
156
|
+
)
|
|
157
|
+
// → [{ label: "Acme", value: 1 }, { label: "Globex", value: 2 }]
|
|
158
|
+
|
|
159
|
+
buildOptions(users, {
|
|
160
|
+
mapLabel: (u) => `${u.firstName} ${u.lastName}`,
|
|
161
|
+
mapValue: (u) => u.id,
|
|
162
|
+
mapDescription: (u) => u.email,
|
|
163
|
+
})
|
|
164
|
+
// → [{ label: "Alex Rivers", value: 101, description: "alex@..." }, ...]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `findOptionLabel(options, value, fallback?)`
|
|
168
|
+
|
|
169
|
+
Reverse lookup — find the display label for a value in an options array.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const OPTS = [{ label: "High", value: "h" }, { label: "Low", value: "l" }];
|
|
173
|
+
findOptionLabel(OPTS, "h") // → "High"
|
|
174
|
+
findOptionLabel(OPTS, "x", "—") // → "—"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## hubspotValues.js
|
|
180
|
+
|
|
181
|
+
Type guards for HubSpot's structured date/time value objects (as emitted by `DateInput`, `TimeInput`, `DateTimeInput`). Use in `filterFn`, `sortComparator`, or anywhere you need to distinguish a HubSpot date-object from a raw string/Date.
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
isDateValueObject({ year: 2026, month: 3, date: 15 }) // → true
|
|
185
|
+
isDateValueObject("2026-04-15") // → false
|
|
186
|
+
isTimeValueObject({ hours: 14, minutes: 30 }) // → true
|
|
187
|
+
isDateTimeValueObject({ date: { year: ... }, time: { ... } }) // → true
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## collections.js
|
|
193
|
+
|
|
194
|
+
### `sumBy(items, keyOrFn)`
|
|
195
|
+
|
|
196
|
+
Sum a numeric property (by key name or accessor fn) across an array. Non-numeric / missing values count as 0.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
sumBy(deals, "amount") // → sum of all amounts
|
|
200
|
+
sumBy(deals, (d) => d.amount * d.probability) // → weighted sum
|
|
201
|
+
sumBy(null, "amount") // → 0
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## tagVariants.js
|
|
207
|
+
|
|
208
|
+
Heuristic mappers from free-form status strings to semantic tag variants. Used internally by `AutoTag` / `AutoStatusTag`, exported so you can reuse the logic in custom cells, sort comparators, etc.
|
|
209
|
+
|
|
210
|
+
### `getAutoTagVariant(value, opts?)`
|
|
211
|
+
|
|
212
|
+
Returns a `Tag` variant — `"success" | "warning" | "error" | "info" | "default"`.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
getAutoTagVariant("Active") // → "success"
|
|
216
|
+
getAutoTagVariant("At risk") // → "warning"
|
|
217
|
+
getAutoTagVariant("Failed") // → "error"
|
|
218
|
+
getAutoTagVariant("New") // → "info"
|
|
219
|
+
getAutoTagVariant("Wibble") // → "default"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Pass `overrides` to force specific values to specific variants, and `fallback` to change the default-case variant.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
getAutoTagVariant("Processing", {
|
|
226
|
+
overrides: { Processing: "warning" },
|
|
227
|
+
fallback: "info",
|
|
228
|
+
})
|
|
229
|
+
// → "warning"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Matches are case-insensitive and tolerant of underscores / dashes / multi-word phrases (`"in_progress"`, `"on hold"`, `"at-risk"` all resolve).
|
|
233
|
+
|
|
234
|
+
### `getAutoStatusTagVariant(value, opts?)`
|
|
235
|
+
|
|
236
|
+
Same as `getAutoTagVariant`, but returns `"danger"` instead of `"error"` (for the `StatusTag` component, which uses the `danger` naming).
|
|
237
|
+
|
|
238
|
+
### `getAutoTagDisplayValue(value)`
|
|
239
|
+
|
|
240
|
+
Normalizes booleans to `"True"` / `"False"`; passes through everything else unchanged. Used when the tag display text needs to be a string but the raw value is a bool.
|
|
241
|
+
|
|
242
|
+
### `createStatusTagSortComparator(opts?)`
|
|
243
|
+
|
|
244
|
+
Builds a sort comparator keyed by the resolved StatusTag variant, then alphabetically within each color group. Drop-in for a `DataTable` column's `sortComparator`.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import { createStatusTagSortComparator } from "hs-uix/utils";
|
|
248
|
+
|
|
249
|
+
<DataTable
|
|
250
|
+
columns={[
|
|
251
|
+
{
|
|
252
|
+
field: "status",
|
|
253
|
+
sortable: true,
|
|
254
|
+
sortComparator: createStatusTagSortComparator(),
|
|
255
|
+
},
|
|
256
|
+
]}
|
|
257
|
+
/>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Default variant ordering: `success → warning → danger/error → info → default`. Override via `variantOrder`, or supply `getLabel` for custom tie-breaking.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## viewAdapters.js
|
|
265
|
+
|
|
266
|
+
Shape transforms for powering "same data, toggle between table and kanban" views. The state props on `DataTable` and `Kanban` are already wire-compatible (data, search, filters, selection, loading, error) — these adapters handle the one part that isn't: the rendering config.
|
|
267
|
+
|
|
268
|
+
### `deriveCardFieldsFromColumns(columns, opts?)`
|
|
269
|
+
|
|
270
|
+
Convert a `DataTable` columns config into a ready-to-use Kanban `cardFields` array.
|
|
271
|
+
|
|
272
|
+
**The common case** — share state, derive card fields from the same columns config:
|
|
273
|
+
|
|
274
|
+
```jsx
|
|
275
|
+
import { DataTable } from "hs-uix/datatable";
|
|
276
|
+
import { Kanban } from "hs-uix/kanban";
|
|
277
|
+
import { deriveCardFieldsFromColumns } from "hs-uix/utils";
|
|
278
|
+
|
|
279
|
+
const COLUMNS = [
|
|
280
|
+
{ field: "name", label: "Deal name", sortable: true, renderCell: (v, row) => <Link href={`/deal/${row.id}`}>{v}</Link> },
|
|
281
|
+
{ field: "owner", label: "Deal owner", sortable: true },
|
|
282
|
+
{ field: "amount", label: "Amount", renderCell: (v) => formatCurrency(v) },
|
|
283
|
+
{ field: "closeDate", label: "Close date", sortable: true },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const CARD_FIELDS = deriveCardFieldsFromColumns(COLUMNS, {
|
|
287
|
+
titleField: "name",
|
|
288
|
+
titleHref: (row) => ({ url: `https://app.hubspot.com/deals/0/deal/${row.id}` }),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const [view, setView] = useState("table");
|
|
292
|
+
|
|
293
|
+
const shared = { data, rowIdField: "id", searchFields: ["name", "owner"], filters, selectable: true, ... };
|
|
294
|
+
|
|
295
|
+
return view === "table"
|
|
296
|
+
? <DataTable {...shared} columns={COLUMNS} />
|
|
297
|
+
: <Kanban {...shared} stages={STAGES} groupBy="status" cardFields={CARD_FIELDS} />;
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Default mapping
|
|
301
|
+
|
|
302
|
+
| DataTable column | Kanban cardField |
|
|
303
|
+
| ---------------- | ---------------- |
|
|
304
|
+
| first column (or `opts.titleField`) | `placement: "title"` |
|
|
305
|
+
| every other column | `placement: "body"` |
|
|
306
|
+
| `col.label` | `field.label` |
|
|
307
|
+
| `col.field` | `field.field` + `field.key` |
|
|
308
|
+
| `col.renderCell(v, row)` | `field.render(v, row)` |
|
|
309
|
+
| `col.truncate` | `field.truncate` |
|
|
310
|
+
| `col.sortable`, `col.sortComparator`, `col.width`, `col.cellWidth`, `col.align`, `col.description`, `col.editable`/`col.edit*` | **dropped** — table-only concepts |
|
|
311
|
+
|
|
312
|
+
### Options
|
|
313
|
+
|
|
314
|
+
| Option | Type | Description |
|
|
315
|
+
| ------ | ---- | ----------- |
|
|
316
|
+
| `titleField` | `string` | Which column's `field` becomes `placement: "title"`. Default: first filtered column. |
|
|
317
|
+
| `titleHref` | `(row) => string \| { url, external? }` | Optional href factory applied to the title field only. Turns the title into a `<Link>` in the card. |
|
|
318
|
+
| `placements` | `Record<string, "title" \| "subtitle" \| "meta" \| "body" \| "footer">` | Per-field placement overrides keyed by `field` name. Wins over `titleField`. |
|
|
319
|
+
| `exclude` | `string[]` | Field names to drop entirely (e.g. `["internalId", "debugMeta"]`). |
|
|
320
|
+
| `include` | `string[]` | Whitelist. If provided, only these fields are emitted. Applied before `exclude` logic. |
|
|
321
|
+
| `maxBodyFields` | `number` | Cap on `placement: "body"` entries emitted. 3–5 is typical for cards — anything more hurts legibility at 350px column widths. |
|
|
322
|
+
|
|
323
|
+
### Examples
|
|
324
|
+
|
|
325
|
+
**Put some columns in the card footer instead of the body:**
|
|
326
|
+
|
|
327
|
+
```jsx
|
|
328
|
+
const CARD_FIELDS = deriveCardFieldsFromColumns(COLUMNS, {
|
|
329
|
+
titleField: "name",
|
|
330
|
+
placements: {
|
|
331
|
+
name: "title",
|
|
332
|
+
owner: "subtitle",
|
|
333
|
+
amount: "footer", // render bottom-right, next to actions
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Skip table-only columns that don't make sense on a card:**
|
|
339
|
+
|
|
340
|
+
```jsx
|
|
341
|
+
const CARD_FIELDS = deriveCardFieldsFromColumns(COLUMNS, {
|
|
342
|
+
exclude: ["lastModifiedBy", "internalNotes", "hubspotScore"],
|
|
343
|
+
titleField: "name",
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Cap the card body to 3 fields (rest are dropped from the card view):**
|
|
348
|
+
|
|
349
|
+
```jsx
|
|
350
|
+
const CARD_FIELDS = deriveCardFieldsFromColumns(COLUMNS, {
|
|
351
|
+
titleField: "name",
|
|
352
|
+
maxBodyFields: 3,
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Full explicit mapping — no heuristics:**
|
|
357
|
+
|
|
358
|
+
```jsx
|
|
359
|
+
const CARD_FIELDS = deriveCardFieldsFromColumns(COLUMNS, {
|
|
360
|
+
include: ["name", "owner", "amount"],
|
|
361
|
+
placements: {
|
|
362
|
+
name: "title",
|
|
363
|
+
owner: "body",
|
|
364
|
+
amount: "footer",
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### What it intentionally doesn't do
|
|
370
|
+
|
|
371
|
+
- **Add selectable action buttons to the card footer.** Use `<KanbanCardActions>` explicitly for those — they're not derivable from table columns.
|
|
372
|
+
- **Port `renderCell` that assumes a table context** (e.g. returns `<TableCell>` elements). If your renderer targets a `<td>`-shaped cell, it'll need a card-compatible version. Plain value formatters and `<Link>` / `<Tag>` / `<Text>` renderers carry over fine.
|
|
373
|
+
- **Adapt sort.** DataTable's per-column sort (click column header) and Kanban's board-wide `sortOptions` are different models — you still maintain a separate `sortOptions` array for Kanban.
|
|
374
|
+
|
|
375
|
+
See also: [Kanban SPEC § cardFields](../kanban/SPEC.md#44-card-rendering--declarative-vs-render-prop).
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## query.js
|
|
380
|
+
|
|
381
|
+
Shared query helpers for collection-style views. These are the same primitives used internally by DataTable, Kanban, Calendar, and Feed's active-filter chips.
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
import {
|
|
385
|
+
buildActiveFilterChips,
|
|
386
|
+
resetFilterValues,
|
|
387
|
+
getEmptyFilterValues,
|
|
388
|
+
filterRows,
|
|
389
|
+
searchRows,
|
|
390
|
+
} from "hs-uix/utils";
|
|
391
|
+
|
|
392
|
+
const [filterValues, setFilterValues] = useState(() => getEmptyFilterValues(filters));
|
|
393
|
+
const activeChips = buildActiveFilterChips(filters, filterValues);
|
|
394
|
+
|
|
395
|
+
const clearFilter = (key) => {
|
|
396
|
+
setFilterValues((prev) => resetFilterValues(filters, prev, key));
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const visibleRows = searchRows(
|
|
400
|
+
filterRows(rows, filters, filterValues),
|
|
401
|
+
search,
|
|
402
|
+
["name", "email"]
|
|
403
|
+
);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### `getEmptyFilterValues(filters, opts?)`
|
|
407
|
+
|
|
408
|
+
Builds a `{ [filter.name]: emptyValue }` object from a filter config list. Defaults are `""` for select filters, `[]` for multiselect, and `{ from: null, to: null }` for date ranges. A filter-level `emptyValue` overrides the select default; pass `getEmptyValue` for broader custom dialects such as Feed's legacy `"all"` empty select value.
|
|
409
|
+
|
|
410
|
+
### `resetFilterValues(filters, values, key, opts?)`
|
|
411
|
+
|
|
412
|
+
Returns a new values object with either one filter reset or all filters reset when `key === "all"`.
|
|
413
|
+
|
|
414
|
+
### `buildActiveFilterChips(filters, values, opts?)`
|
|
415
|
+
|
|
416
|
+
Returns `[{ key, label }]` descriptors for active filters. Supports select labels, multiselect joined labels, date range labels, custom active detection, and custom date formatting.
|
|
417
|
+
|
|
418
|
+
### `filterRows(rows, filters, values)` / `searchRows(rows, term, fields, opts?)`
|
|
419
|
+
|
|
420
|
+
Pure in-memory filtering/search helpers used by DataTable, Kanban, and Calendar. Feed intentionally keeps its own row pipeline because it supports path/accessor-based values and string-coercing equality.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## crmSearchAdapters.js
|
|
425
|
+
|
|
426
|
+
CRM-bound data components and the search plumbing behind them. `CrmDataTable` and `CrmKanban` are the high-level entry points — point them at a CRM `objectType` + `properties` and you get a fully wired table or board with no manual data-source code.
|
|
427
|
+
|
|
428
|
+
### Pagination model
|
|
429
|
+
|
|
430
|
+
By default both components **fetch one batch (`pageLength`, default 100) and do search / sort / filter / pagination client-side** — a single request, no refetch per interaction, and pagination "just works" via in-memory slicing. When the result set exceeds the batch they show a "first N of M" note rather than silently showing a partial view. Pass **`serverSide`** to force search/filter/sort to run as fresh CRM queries from the first render. A built-in sort translator maps the active column/board sort to CRM `sorts` (honoring `propertyMap`) in server-query mode, so you don't hand-write a `sortMap`.
|
|
431
|
+
|
|
432
|
+
HubSpot fixed the `useCrmSearch().pagination.nextPage()` offset issue in platform `2026.03`; the lower-level hooks now also expose `pagination` / `hasMore` from the native response for custom views. `CrmDataTable` now uses that native cursor to lazy-load additional batches: with `pageLength={100}` and `pageSize={10}`, clicking table page 11 fetches the next CRM batch and appends it before rendering that page. `CrmKanban` exposes the same batch loading through its column `Load more` affordance when more CRM results are available.
|
|
433
|
+
|
|
434
|
+
> Note: these are JSX components that live in `utils` because they're CRM-data adapters; the underlying `useCrmSearch*` hooks live here too.
|
|
435
|
+
|
|
436
|
+
### `CrmDataTable`
|
|
437
|
+
|
|
438
|
+
A `DataTable` bound to CRM search. Accepts all `DataTable` props except the data-source ones it manages for you (`data`, `loading`, `error`, `searchValue`, `onParamsChange`).
|
|
439
|
+
|
|
440
|
+
```jsx
|
|
441
|
+
import { CrmDataTable } from "hs-uix/utils";
|
|
442
|
+
|
|
443
|
+
<CrmDataTable
|
|
444
|
+
objectType="deal"
|
|
445
|
+
properties={["dealname", "amount", "dealstage", "closedate"]}
|
|
446
|
+
columns={[
|
|
447
|
+
{ field: "dealname", label: "Deal", sortable: true },
|
|
448
|
+
{ field: "amount", label: "Amount", renderCell: (v) => formatCurrency(v) },
|
|
449
|
+
{ field: "dealstage", label: "Stage" },
|
|
450
|
+
]}
|
|
451
|
+
searchFields={["dealname"]}
|
|
452
|
+
autoFilters={["dealstage"]}
|
|
453
|
+
/>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### `CrmKanban`
|
|
457
|
+
|
|
458
|
+
The board analog of `CrmDataTable`. `stages` is optional — pass it for real pipeline labels, or let stages auto-derive from the batch (labelled via `stageLabels`).
|
|
459
|
+
|
|
460
|
+
```jsx
|
|
461
|
+
import { CrmKanban } from "hs-uix/utils";
|
|
462
|
+
|
|
463
|
+
<CrmKanban
|
|
464
|
+
objectType="deal"
|
|
465
|
+
properties={["dealname", "amount", "dealstage"]}
|
|
466
|
+
groupBy="dealstage"
|
|
467
|
+
stageLabels={{ appointmentscheduled: "Appointment", qualifiedtobuy: "Qualified" }}
|
|
468
|
+
cardFields={[
|
|
469
|
+
{ field: "dealname", placement: "title" },
|
|
470
|
+
{ field: "amount", placement: "meta", render: (v) => formatCurrency(v) },
|
|
471
|
+
]}
|
|
472
|
+
/>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Shared props
|
|
476
|
+
|
|
477
|
+
| Prop | Type | Default | Description |
|
|
478
|
+
| ---- | ---- | ------- | ----------- |
|
|
479
|
+
| `objectType` | string | — | CRM object to query (`"contact"`, `"company"`, `"deal"`, or any object type id/name). |
|
|
480
|
+
| `properties` | `string[]` | — | CRM properties to fetch. |
|
|
481
|
+
| `pageLength` | number | `100` | Batch size fetched per query. |
|
|
482
|
+
| `serverSide` | boolean | `false` | Force search/filter/sort to refetch from CRM instead of waiting until the first batch is capped. Table/board pagination stays client-side over loaded rows, and loads the next CRM cursor batch as needed. |
|
|
483
|
+
| `autoFilters` | `boolean \| string[] \| { fields? }` | — | Auto-generate select filters from properties (optionally capped by `autoFilterMaxOptions`). |
|
|
484
|
+
| `propertyMap` | `Record<string,string>` | — | Map your field names to CRM property names (used for sorts/filters). |
|
|
485
|
+
| `filterMap` / `sortMap` | fn | — | Advanced overrides for translating filters/sorts to CRM config. |
|
|
486
|
+
| `searchFields` | `string[]` | — | Fields the search box queries. |
|
|
487
|
+
| `mapRecord` | `(record) => Row` | — | Customize how a raw CRM record becomes a row. |
|
|
488
|
+
| `dataTableProps` / `kanbanProps` | object | — | Escape hatch to pass anything straight through to the underlying `DataTable` / `Kanban`. |
|
|
489
|
+
|
|
490
|
+
`CrmKanban` additionally takes `groupBy` (required), `stages` (optional), and `stageLabels`.
|
|
491
|
+
|
|
492
|
+
### Lower-level building blocks
|
|
493
|
+
|
|
494
|
+
If you need to drive a custom view, the hooks and helpers are exported directly:
|
|
495
|
+
|
|
496
|
+
- `useCrmSearchDataSource(params, options)` — the hook both components use; returns `{ data, loading, error, totalCount, pagination, hasMore, ... }` for a CRM search.
|
|
497
|
+
- `useCrmSearchOptions(params, options)` — CRM search shaped into `{ label, value }` options for a `Select`.
|
|
498
|
+
- `buildCrmSearchConfig(params, options)` — build the CRM search request config (appends a stable `hs_object_id` sort tiebreaker so cursor paging is deterministic).
|
|
499
|
+
- `normalizeCrmSearchRecord` / `normalizeCrmSearchRows` — flatten raw CRM responses into plain rows.
|
|
500
|
+
- `crmSearchResultToOption` — map a single CRM record into an option.
|
|
501
|
+
- `makeCrmSearchSelectField` / `makeCrmSearchMultiSelectField` — build FormBuilder field configs backed by CRM search.
|
|
502
|
+
- `resolveCrmObjectType` — normalize object-type aliases (`"contact"` ↔ `"contacts"`, etc.).
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Guidelines
|
|
507
|
+
|
|
508
|
+
- Keep helpers pure and side-effect free
|
|
509
|
+
- Prefer small focused utilities over broad catch-all helpers
|
|
510
|
+
- Put JSX wrappers in `src/common-components/`
|
|
511
|
+
- All formatters accept a trailing options object that spreads into the underlying `Intl` call — reach for that before inventing a new formatter
|
package/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
|
-
import type { DataTableProps, DataTableColumn } from "./
|
|
3
|
-
import type { KanbanProps } from "./
|
|
2
|
+
import type { DataTableProps, DataTableColumn } from "./src/datatable/index";
|
|
3
|
+
import type { KanbanProps } from "./src/kanban/index";
|
|
4
4
|
|
|
5
5
|
export type AutoStatusTagVariant = "default" | "success" | "warning" | "danger" | "info";
|
|
6
6
|
export type AutoTagVariant = "default" | "success" | "warning" | "error" | "info";
|
|
@@ -39,6 +39,46 @@ export interface BuiltOption {
|
|
|
39
39
|
description?: unknown;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface QueryFilterConfig<Row = Record<string, unknown>> {
|
|
43
|
+
name: string;
|
|
44
|
+
type?: "select" | "multiselect" | "dateRange" | string;
|
|
45
|
+
label?: string;
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
chipLabel?: string;
|
|
48
|
+
emptyValue?: unknown;
|
|
49
|
+
options?: Array<{ label: unknown; value: unknown }>;
|
|
50
|
+
filterFn?: (row: Row, value: unknown) => boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ActiveFilterChip {
|
|
54
|
+
key: string;
|
|
55
|
+
label: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FilterResetOptions {
|
|
59
|
+
getEmptyValue?: (filter: QueryFilterConfig) => unknown;
|
|
60
|
+
fallbackEmptyValue?: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BuildActiveFilterChipsOptions {
|
|
64
|
+
isFilterActive?: (filter: QueryFilterConfig, value: unknown) => boolean;
|
|
65
|
+
formatDate?: (value: unknown) => string;
|
|
66
|
+
dateFromPrefix?: string;
|
|
67
|
+
dateToPrefix?: string;
|
|
68
|
+
dateJoiner?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export declare function getEmptyFilterValue(filter: QueryFilterConfig): unknown;
|
|
72
|
+
export declare function getEmptyFilterValues(filters?: QueryFilterConfig[], options?: FilterResetOptions): Record<string, unknown>;
|
|
73
|
+
export declare function resetFilterValues(filters?: QueryFilterConfig[], values?: Record<string, unknown>, key?: string, options?: FilterResetOptions): Record<string, unknown>;
|
|
74
|
+
export declare function isFilterActive(filter: QueryFilterConfig, value: unknown): boolean;
|
|
75
|
+
export declare function formatDateChip(dateObj: unknown): string;
|
|
76
|
+
export declare function dateToTimestamp(dateObj: unknown): number | null;
|
|
77
|
+
export declare function buildActiveFilterChips(filters?: QueryFilterConfig[], values?: Record<string, unknown>, options?: BuildActiveFilterChipsOptions): ActiveFilterChip[];
|
|
78
|
+
export declare function toStableKey(value: unknown): string;
|
|
79
|
+
export declare function filterRows<Row = Record<string, unknown>>(rows: Row[], filters?: QueryFilterConfig<Row>[], values?: Record<string, unknown>): Row[];
|
|
80
|
+
export declare function searchRows<Row = Record<string, unknown>>(rows: Row[], term: string | null | undefined, fields?: string[], opts?: { fuzzy?: boolean; fuzzyOptions?: Record<string, unknown> }): Row[];
|
|
81
|
+
|
|
42
82
|
export declare function getAutoTagVariant(value: unknown, options?: AutoTagOptions): AutoTagVariant;
|
|
43
83
|
export declare function getAutoStatusTagVariant(value: unknown, options?: AutoStatusTagOptions): AutoStatusTagVariant;
|
|
44
84
|
export declare function getAutoTagDisplayValue(value: unknown): unknown;
|
|
@@ -115,10 +155,22 @@ export interface CrmSearchOptionOptions<Row = Record<string, unknown>> {
|
|
|
115
155
|
mapOption?: (row: Row) => BuiltOption;
|
|
116
156
|
}
|
|
117
157
|
|
|
158
|
+
export interface CrmSearchPagination {
|
|
159
|
+
hasNextPage: boolean;
|
|
160
|
+
hasPreviousPage: boolean;
|
|
161
|
+
currentPage: number;
|
|
162
|
+
pageSize: number;
|
|
163
|
+
nextPage: () => void;
|
|
164
|
+
previousPage: () => void;
|
|
165
|
+
reset: () => void;
|
|
166
|
+
}
|
|
167
|
+
|
|
118
168
|
export interface CrmSearchDataSource<Row = Record<string, unknown>> {
|
|
119
169
|
data: Row[];
|
|
120
170
|
rows: Row[];
|
|
121
171
|
response: unknown;
|
|
172
|
+
pagination?: CrmSearchPagination;
|
|
173
|
+
hasMore: boolean;
|
|
122
174
|
loading: boolean;
|
|
123
175
|
isLoading: boolean;
|
|
124
176
|
error: string | boolean;
|
|
@@ -136,7 +188,7 @@ export interface CrmSearchFormField<Field = Record<string, unknown>> extends Rec
|
|
|
136
188
|
loading?: boolean;
|
|
137
189
|
}
|
|
138
190
|
|
|
139
|
-
export interface CrmDataTableProps<Row = Record<string, unknown>> extends Omit<DataTableProps<Row>, "data" | "loading" | "error" | "columns" | "searchValue" | "onParamsChange"> {
|
|
191
|
+
export interface CrmDataTableProps<Row = Record<string, unknown>> extends Omit<DataTableProps<Row>, "data" | "loading" | "error" | "columns" | "searchValue" | "onParamsChange" | "totalCount" | "clientTotalCount"> {
|
|
140
192
|
objectType: "contact" | "contacts" | "company" | "companies" | "deal" | "deals" | string;
|
|
141
193
|
properties?: string[];
|
|
142
194
|
columns?: DataTableColumn<Row>[];
|
|
File without changes
|