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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/experimental/index";
|
package/filter.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/filter/index";
|
package/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// Each component module prefixes its type names (DataTable*, Kanban*, Feed*,
|
|
2
|
-
// Calendar*, FormBuilder*) and its value exports are unique, so
|
|
3
|
-
// collision-free and keeps this root barrel automatically in
|
|
4
|
-
// type each component declares — no hand-maintained list to
|
|
2
|
+
// Calendar*, FormBuilder*, and Filter*) and its value exports are unique, so
|
|
3
|
+
// `export *` is collision-free and keeps this root barrel automatically in
|
|
4
|
+
// sync with every type each component declares — no hand-maintained list to
|
|
5
|
+
// fall out of date.
|
|
5
6
|
export * from "./src/datatable/index";
|
|
6
7
|
export * from "./src/kanban/index";
|
|
7
8
|
export * from "./src/feed/index";
|
|
8
9
|
export * from "./src/calendar/index";
|
|
9
10
|
export * from "./src/form/index";
|
|
11
|
+
export * from "./src/filter/index";
|
|
12
|
+
// safe prefixes everything with Safe* / catalog-style ALL_CAPS names, so it's
|
|
13
|
+
// also collision-free under export *.
|
|
14
|
+
export * from "./src/safe/index";
|
|
10
15
|
|
|
11
16
|
export {
|
|
12
17
|
ActiveFilterChips,
|
|
@@ -16,10 +21,20 @@ export {
|
|
|
16
21
|
CollectionFilterControl,
|
|
17
22
|
CollectionSortSelect,
|
|
18
23
|
CollectionToolbar,
|
|
24
|
+
compareHsDateValues,
|
|
19
25
|
CrmLookupSelect,
|
|
26
|
+
CrmRecordPicker,
|
|
27
|
+
DATE_FILTER_OPERATORS,
|
|
28
|
+
DATE_RANGE_CUSTOM_VALUE,
|
|
29
|
+
DATE_ROLLING_UNIT_OPTIONS,
|
|
30
|
+
DateRangePicker,
|
|
20
31
|
formatCollectionCount,
|
|
32
|
+
isValidDateRange,
|
|
33
|
+
presetToRange,
|
|
34
|
+
toHsDateValue,
|
|
21
35
|
KeyValueList,
|
|
22
36
|
SectionHeader,
|
|
37
|
+
SKELETON_FILL,
|
|
23
38
|
Spinner,
|
|
24
39
|
SPINNERS,
|
|
25
40
|
SPINNER_NAMES,
|
|
@@ -55,6 +70,7 @@ export {
|
|
|
55
70
|
DEFAULT_SVG_FONT_WEIGHT,
|
|
56
71
|
} from "./common-components";
|
|
57
72
|
export {
|
|
73
|
+
applyPatches,
|
|
58
74
|
buildActiveFilterChips,
|
|
59
75
|
buildCrmSearchConfig,
|
|
60
76
|
buildOptions,
|
|
@@ -89,6 +105,8 @@ export {
|
|
|
89
105
|
export type {
|
|
90
106
|
AutoTagOptions,
|
|
91
107
|
AutoTagVariant,
|
|
108
|
+
JsonPatchOp,
|
|
109
|
+
JsonPatchOperation,
|
|
92
110
|
AutoStatusTagOptions,
|
|
93
111
|
AutoStatusTagVariant,
|
|
94
112
|
StatusTagSortComparatorOptions,
|
|
@@ -124,8 +142,32 @@ export type {
|
|
|
124
142
|
CollectionSortSelectProps,
|
|
125
143
|
CollectionToolbarProps,
|
|
126
144
|
CrmLookupSelectProps,
|
|
145
|
+
CrmRecordPickerCreateConfig,
|
|
146
|
+
CrmRecordPickerCreateOptionRules,
|
|
147
|
+
CrmRecordPickerFieldAccessor,
|
|
148
|
+
CrmRecordPickerId,
|
|
149
|
+
CrmRecordPickerOption,
|
|
150
|
+
CrmRecordPickerOptionConfig,
|
|
151
|
+
CrmRecordPickerProps,
|
|
152
|
+
CrmRecordPickerRecord,
|
|
153
|
+
CrmRecordPickerSelection,
|
|
154
|
+
CrmRecordPickerValue,
|
|
127
155
|
DateDirectionLabels,
|
|
128
156
|
DatePresetOption,
|
|
157
|
+
DateRangePickerChangeMeta,
|
|
158
|
+
DateRangePickerDateValue,
|
|
159
|
+
DateRangePickerExplicitRangeValue,
|
|
160
|
+
DateRangePickerOperator,
|
|
161
|
+
DateRangePickerPresetOption,
|
|
162
|
+
DateRangePickerPresetValue,
|
|
163
|
+
DateRangePickerPresenceValue,
|
|
164
|
+
DateRangePickerProps,
|
|
165
|
+
DateRangePickerRangeValue,
|
|
166
|
+
DateRangePickerRollingDirection,
|
|
167
|
+
DateRangePickerRollingUnit,
|
|
168
|
+
DateRangePickerRollingValue,
|
|
169
|
+
DateRangePickerSingleDateValue,
|
|
170
|
+
DateRangePickerValue,
|
|
129
171
|
KeyValueListItem,
|
|
130
172
|
KeyValueListProps,
|
|
131
173
|
SectionHeaderProps,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hs-uix",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Production-ready UI components for HubSpot UI Extensions — DataTable, FormBuilder, and more",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
"import": "./dist/calendar.mjs",
|
|
38
38
|
"require": "./dist/calendar.js"
|
|
39
39
|
},
|
|
40
|
+
"./filter": {
|
|
41
|
+
"types": "./filter.d.ts",
|
|
42
|
+
"import": "./dist/filter.mjs",
|
|
43
|
+
"require": "./dist/filter.js"
|
|
44
|
+
},
|
|
45
|
+
"./experimental": {
|
|
46
|
+
"types": "./experimental.d.ts",
|
|
47
|
+
"import": "./dist/experimental.mjs",
|
|
48
|
+
"require": "./dist/experimental.js"
|
|
49
|
+
},
|
|
40
50
|
"./common-components": {
|
|
41
51
|
"types": "./common-components.d.ts",
|
|
42
52
|
"import": "./dist/common-components.mjs",
|
|
@@ -46,6 +56,11 @@
|
|
|
46
56
|
"types": "./utils.d.ts",
|
|
47
57
|
"import": "./dist/utils.mjs",
|
|
48
58
|
"require": "./dist/utils.js"
|
|
59
|
+
},
|
|
60
|
+
"./safe": {
|
|
61
|
+
"types": "./safe.d.ts",
|
|
62
|
+
"import": "./dist/safe.mjs",
|
|
63
|
+
"require": "./dist/safe.js"
|
|
49
64
|
}
|
|
50
65
|
},
|
|
51
66
|
"files": [
|
|
@@ -56,8 +71,11 @@
|
|
|
56
71
|
"kanban.d.ts",
|
|
57
72
|
"feed.d.ts",
|
|
58
73
|
"calendar.d.ts",
|
|
74
|
+
"filter.d.ts",
|
|
75
|
+
"experimental.d.ts",
|
|
59
76
|
"common-components.d.ts",
|
|
60
77
|
"utils.d.ts",
|
|
78
|
+
"safe.d.ts",
|
|
61
79
|
"src/**/*.d.ts",
|
|
62
80
|
"README.md"
|
|
63
81
|
],
|
package/safe.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/safe/index";
|
package/src/calendar/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/hs-uix)
|
|
5
5
|
[](https://github.com/05bmckay/hs-uix/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
A presentational calendar surface for HubSpot UI Extensions. Hand it an array of records plus an `eventFields` map and it renders a **Month**, **Week**, **Day**, or **
|
|
7
|
+
A presentational calendar surface for HubSpot UI Extensions. Hand it an array of records plus an `eventFields` map and it renders a **Month**, **Week**, **Day**, **Agenda**, or **Resource** view with a Today / ‹ › / view-switcher toolbar, optional search + filters, click-to-open event overlays (Popover, Modal, or Panel), and an optional drag-free **reschedule** affordance. Like Kanban and Feed, the calendar is data-driven and presentational — the caller owns fetching *and persisting*: even a reschedule only **emits** the new `{ start, end }`, it never mutates the events you passed in.
|
|
8
8
|
|
|
9
9
|
```jsx
|
|
10
10
|
import { Calendar } from "hs-uix/calendar";
|
|
@@ -36,7 +36,8 @@ const fields = {
|
|
|
36
36
|
|
|
37
37
|
## Features
|
|
38
38
|
|
|
39
|
-
-
|
|
39
|
+
- Five views behind a `Select` switcher: **Month** (7-column day grid), **Week** / **Day** (hour-row time grid), **Agenda** (day-grouped list), and **Resource** (rows = owners/rooms/teams × the focused week's days — only offered when `resources` / `resourceField` is provided)
|
|
40
|
+
- **Drag-free reschedule** (the platform has no drag-and-drop): `rescheduleOptions` adds preset buttons ("+1 hour", "+1 day", "Next week", custom shifts or accessors) plus a "Pick date" `DateInput` to the event-detail overlay; `onEventReschedule` emits the shifted `{ start, end }` with the event's duration preserved (DST-safe) — you persist, the Calendar never moves events itself
|
|
40
41
|
- Controlled or uncontrolled `view` and `focusedDate`, with `onViewChange` / `onNavigate`
|
|
41
42
|
- Date coercion for every shape a HubSpot field arrives in: `Date`, epoch ms (number **or** string), ISO string, or a `DateInput` value object (`{ year, month, date }`, 0-indexed month) — date-only strings are parsed as **local** midnight so events never land a day early
|
|
42
43
|
- Multi-day events render across each day they touch; timed events show their start time
|
|
@@ -73,6 +74,7 @@ Requires `@hubspot/ui-extensions` >= 0.14.0 and `react` >= 18.0.0 as peer depend
|
|
|
73
74
|
| `week` | An hour-row time grid for the focused week, plus an all-day band. | ± 1 week |
|
|
74
75
|
| `day` | The single-day hour schedule (the same grid as week, one wide column) with an all-day band and a "now" current-hour marker. | ± 1 day |
|
|
75
76
|
| `agenda` | The focused week's events grouped under day headers (time · title · owner rows). | ± 1 week |
|
|
77
|
+
| `resource` | Rows = resources (owners / rooms / teams), columns = the focused week's days. Each cell stacks the same chips as a month cell, with the same "+N more" overflow popover. **Only joins the switcher when `resources` or `resourceField` is provided.** | ± 1 week |
|
|
76
78
|
|
|
77
79
|
The view switcher is a `Select` (not `Tabs`) because the platform caches `Tabs` bodies and they won't re-render on data changes.
|
|
78
80
|
|
|
@@ -130,6 +132,67 @@ A rep's week in an hour-row grid, Monday start, 8 AM–6 PM, opening a Panel per
|
|
|
130
132
|
/>
|
|
131
133
|
```
|
|
132
134
|
|
|
135
|
+
### Resource lanes (owners / rooms / teams)
|
|
136
|
+
|
|
137
|
+
Rows = resources, columns = the focused week. `resourceField` (a key or accessor on **your** record) lanes each event; events whose id matches no declared resource get an appended derived lane (labeled from `resourceLabels` or the raw id), and events with **no** id land in a trailing "Unassigned" lane.
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
<Calendar
|
|
141
|
+
events={meetings}
|
|
142
|
+
eventFields={{ id: "id", start: "start", end: "end", title: "title", color: "color" }}
|
|
143
|
+
defaultView="resource"
|
|
144
|
+
resources={[
|
|
145
|
+
{ id: "u1", label: "Ana Souza" },
|
|
146
|
+
{ id: "u2", label: "Ben Liu" },
|
|
147
|
+
{ id: "room-a", label: "Conference Room A" },
|
|
148
|
+
]}
|
|
149
|
+
resourceField="ownerId" // or (meeting) => meeting.owner?.id
|
|
150
|
+
resourceLabels={{ "u3": "Cara Diaz" }} // labels for derived (undeclared) lanes
|
|
151
|
+
showUnassignedLane // default true; false omits id-less events
|
|
152
|
+
weekStartsOn={1}
|
|
153
|
+
hideWeekends
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- Declared `resources` always render, in order, even with zero events — so a free room reads as *free*.
|
|
158
|
+
- Ids are matched by `String(id)`, so a numeric `7` and a string `"7"` are the same lane.
|
|
159
|
+
- The "Unassigned" lane renders only when it has events; relabel it via `labels.unassigned`.
|
|
160
|
+
- The view appears in the switcher **only** when `resources` or `resourceField` is provided.
|
|
161
|
+
|
|
162
|
+
### Drag-free reschedule
|
|
163
|
+
|
|
164
|
+
The platform has no drag-and-drop, so rescheduling is explicit: `rescheduleOptions` adds preset buttons and a "Pick date" `DateInput` to the event-detail overlay. The Calendar computes the new range — **both** endpoints shifted, duration preserved — and **emits** it. You persist and pass updated `events` back in; the Calendar never mutates its own events (the same presentational contract as Kanban).
|
|
165
|
+
|
|
166
|
+
```jsx
|
|
167
|
+
// true → "+1 hour" / "+1 day" / "Next week" + the date picker:
|
|
168
|
+
<Calendar
|
|
169
|
+
events={tasks}
|
|
170
|
+
eventFields={fields}
|
|
171
|
+
rescheduleOptions
|
|
172
|
+
onEventReschedule={async (raw, { start, end }, event) => {
|
|
173
|
+
await patchTask(raw.id, { start, end }); // you persist…
|
|
174
|
+
refetch(); // …and feed updated events back in
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
// Custom presets — relative shifts, or accessors returning the new start:
|
|
179
|
+
<Calendar
|
|
180
|
+
events={tasks}
|
|
181
|
+
eventFields={fields}
|
|
182
|
+
overlayMode="panel" // roomier home for the picker than the experimental popover
|
|
183
|
+
rescheduleOptions={[
|
|
184
|
+
{ label: "+30 min", shift: { minutes: 30 } },
|
|
185
|
+
{ label: "+2 days", shift: { days: 2 } },
|
|
186
|
+
{ label: "Next sprint", shift: (event) => nextSprintStart(event.raw) },
|
|
187
|
+
]}
|
|
188
|
+
onEventReschedule={persistRange}
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
DST semantics (tested against America/Chicago): day/week shifts are **calendar** shifts — a 9 AM meeting moved across the spring-forward weekend is still 9 AM, and a midnight-to-midnight all-day span stays anchored to midnight; hour/minute shifts are **exact clock durations** ("+1 hour" always moves the instant 60 real minutes). "Pick date…" keeps the event's original time-of-day. When the timezone layer is engaged, the math runs on your **original** instants (`event.sourceStart` / `sourceEnd`), not the converted display dates, so the emitted range stays in your data's time domain.
|
|
193
|
+
|
|
194
|
+
A custom `renderEventDetail` replaces the whole overlay body, reschedule section included — wire your own buttons there if you override it.
|
|
195
|
+
|
|
133
196
|
### Server-driven data
|
|
134
197
|
|
|
135
198
|
Fetch only the visible range. `onRangeChange` fires on mount and on every navigation / view change.
|
|
@@ -223,15 +286,23 @@ How it works: each event instant is converted to a "wall-clock" `Date` in the ac
|
|
|
223
286
|
| `view` | `CalendarView` | — | Controlled current view. |
|
|
224
287
|
| `defaultView` | `CalendarView` | `"month"` | Uncontrolled initial view. |
|
|
225
288
|
| `onViewChange` | `(view) => void` | — | Fires when the view changes. |
|
|
226
|
-
| `views` | `CalendarView[]` | all | Which views to expose in the switcher (month / week / day / agenda). |
|
|
289
|
+
| `views` | `CalendarView[]` | all | Which views to expose in the switcher (month / week / day / agenda / resource — resource still requires `resources`/`resourceField`). |
|
|
227
290
|
| `focusedDate` | `CalendarDateInput` | — | Controlled focused date. |
|
|
228
291
|
| `defaultFocusedDate` | `CalendarDateInput` | today | Uncontrolled initial date. |
|
|
229
292
|
| `onNavigate` | `(date, { view }) => void` | — | Fires on prev / next / today. |
|
|
230
293
|
| `weekStartsOn` | `0 \| 1` | `0` | 0 = Sunday, 1 = Monday. |
|
|
231
294
|
| `hideWeekends` | `boolean` | `false` | Hide Sat/Sun in month + week views. |
|
|
232
295
|
| `maxEventsPerDay` | `number` | `3` | Chips per month cell before "+N more". |
|
|
296
|
+
| `monthEventStyle` | `"statusTag" \| "tag"` | `"statusTag"` | Month-cell event token: `statusTag` (colored dot + text, matches week/day) or `tag` (bordered pill). Both truncate and hold a fixed height as columns narrow. |
|
|
297
|
+
| `monthEventMaxChars` | `number` | per-style, from column width | Max characters for a month-cell label before "…". Labels are truncated up front so every token truncates consistently (the tag's native truncation measures unreliably while the table is still sizing columns). |
|
|
233
298
|
| `dayStartHour` | `number` | `8` | First hour row in week/day (0–23). |
|
|
234
299
|
| `dayEndHour` | `number` | `20` | Last hour row in week/day (0–23). |
|
|
300
|
+
| `resources` | `(id \| { id, label? })[]` | — | Declared resource lanes, in order. Always render, even when empty. Providing this (or `resourceField`) adds the `resource` view to the switcher. |
|
|
301
|
+
| `resourceField` | `string \| (event) => id` | — | How to read an event's resource id from your record. `null` / `""` ⇒ unassigned. Ids matched by `String(id)`; undeclared ids get appended derived lanes. |
|
|
302
|
+
| `resourceLabels` | `{ [id]: label }` | — | Labels for resources declared without one and for derived lanes. Falls back to `String(id)`. |
|
|
303
|
+
| `showUnassignedLane` | `boolean` | `true` | Trailing "Unassigned" lane for id-less events (rendered only when non-empty). `false` omits those events from the resource view. |
|
|
304
|
+
| `rescheduleOptions` | `true \| (option \| fn)[]` | — | Adds reschedule presets + a "Pick date" `DateInput` to the event overlay. `true` ⇒ "+1 hour" / "+1 day" / "Next week". Entries: `{ label, shift: { days?, weeks?, hours?, minutes? } }`, `{ label, shift: (event) => newStart }`, or a bare `(event) => newStart`. |
|
|
305
|
+
| `onEventReschedule` | `(raw, { start, end }, event) => void` | — | Fires when a reschedule affordance is used. Both endpoints shifted, duration preserved (DST-safe), computed from your original instants. **You persist** — the Calendar never mutates its events. |
|
|
235
306
|
| `timeZone` | `string` | — | Controlled IANA zone (e.g. `"America/Chicago"`). Setting it opts into the tz layer; all times/placement/grouping resolve here. |
|
|
236
307
|
| `defaultTimeZone` | `string` | — | Uncontrolled initial zone. Opts into the tz layer; the layer itself starts at `"UTC"` if engaged without this. |
|
|
237
308
|
| `onTimeZoneChange` | `(tz) => void` | — | Fires when the zone changes. |
|
|
@@ -275,10 +346,10 @@ How it works: each event instant is converted to a "wall-clock" `Date` in the ac
|
|
|
275
346
|
Render callbacks (`renderEventDetail`, `onEventClick`, `renderDayCell`) receive the normalized event:
|
|
276
347
|
|
|
277
348
|
```ts
|
|
278
|
-
{ key, id, start: Date | null, end: Date | null, title, subtitle, color, href, raw }
|
|
349
|
+
{ key, id, start: Date | null, end: Date | null, sourceStart, sourceEnd, title, subtitle, color, href, raw }
|
|
279
350
|
```
|
|
280
351
|
|
|
281
|
-
`raw` is your original record.
|
|
352
|
+
`raw` is your original record. `start` / `end` are the dates the calendar *displays* (wall-clock converted when the timezone layer is engaged); `sourceStart` / `sourceEnd` are your **original** instants, which is what the reschedule math runs on.
|
|
282
353
|
|
|
283
354
|
---
|
|
284
355
|
|
package/src/calendar/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
4
4
|
// Primitives
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
export type CalendarView = "month" | "week" | "day" | "agenda";
|
|
7
|
+
export type CalendarView = "month" | "week" | "day" | "agenda" | "resource";
|
|
8
8
|
export type CalendarOverlayMode = "popover" | "modal" | "panel" | "none";
|
|
9
9
|
export type CalendarFilterType = "select" | "multiselect" | "dateRange";
|
|
10
10
|
|
|
@@ -54,6 +54,12 @@ export interface CalendarNormalizedEvent<Event = Record<string, unknown>> {
|
|
|
54
54
|
id: string | number | undefined;
|
|
55
55
|
start: Date | null;
|
|
56
56
|
end: Date | null;
|
|
57
|
+
/** The UN-converted start instant from your data (no timezone-layer wall-clock
|
|
58
|
+
* conversion). Reschedule math runs on this, so emitted dates stay in your
|
|
59
|
+
* data's time domain. */
|
|
60
|
+
sourceStart: Date | null;
|
|
61
|
+
/** The UN-converted end instant (mirrors `sourceStart` when the event has no end). */
|
|
62
|
+
sourceEnd: Date | null;
|
|
57
63
|
title: ReactNode;
|
|
58
64
|
subtitle: ReactNode;
|
|
59
65
|
color: string | undefined;
|
|
@@ -61,6 +67,60 @@ export interface CalendarNormalizedEvent<Event = Record<string, unknown>> {
|
|
|
61
67
|
raw: Event;
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Resource / lane view
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/** A declared resource lane: `{ id, label? }`, or a bare id. Declared lanes
|
|
75
|
+
* always render (in order), even with zero events. */
|
|
76
|
+
export type CalendarResource =
|
|
77
|
+
| string
|
|
78
|
+
| number
|
|
79
|
+
| { id: string | number; label?: ReactNode };
|
|
80
|
+
|
|
81
|
+
/** How to read an event's resource id: a key on the raw event, or an accessor.
|
|
82
|
+
* `null` / `undefined` / `""` mean unassigned. Ids are matched by `String(id)`. */
|
|
83
|
+
export type CalendarResourceField<Event = Record<string, unknown>> =
|
|
84
|
+
| string
|
|
85
|
+
| ((event: Event) => string | number | null | undefined);
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Drag-free reschedule
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** A relative shift. Days/weeks are CALENDAR shifts (wall-clock time-of-day
|
|
92
|
+
* preserved on both endpoints, DST-safe); hours/minutes are exact clock
|
|
93
|
+
* durations. */
|
|
94
|
+
export interface CalendarRescheduleShift {
|
|
95
|
+
days?: number;
|
|
96
|
+
weeks?: number;
|
|
97
|
+
hours?: number;
|
|
98
|
+
minutes?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One reschedule affordance: a labeled relative shift, a labeled accessor
|
|
102
|
+
* returning the new start (any `CalendarDateInput` shape), or a bare accessor
|
|
103
|
+
* (labeled from `fn.name`, falling back to "Reschedule"). */
|
|
104
|
+
export type CalendarRescheduleOption<Event = Record<string, unknown>> =
|
|
105
|
+
| {
|
|
106
|
+
label: string;
|
|
107
|
+
shift:
|
|
108
|
+
| CalendarRescheduleShift
|
|
109
|
+
| ((event: CalendarNormalizedEvent<Event>) => CalendarDateInput | null | undefined);
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
label: string;
|
|
113
|
+
getStart: (event: CalendarNormalizedEvent<Event>) => CalendarDateInput | null | undefined;
|
|
114
|
+
}
|
|
115
|
+
| ((event: CalendarNormalizedEvent<Event>) => CalendarDateInput | null | undefined);
|
|
116
|
+
|
|
117
|
+
/** The new range emitted by `onEventReschedule` — BOTH endpoints shifted, the
|
|
118
|
+
* event's duration/shape preserved (DST-safe). */
|
|
119
|
+
export interface CalendarRescheduleRange {
|
|
120
|
+
start: Date;
|
|
121
|
+
end: Date;
|
|
122
|
+
}
|
|
123
|
+
|
|
64
124
|
export interface CalendarRange {
|
|
65
125
|
start: Date;
|
|
66
126
|
end: Date;
|
|
@@ -101,6 +161,10 @@ export interface CalendarLabels {
|
|
|
101
161
|
errorMessage?: string;
|
|
102
162
|
open?: string;
|
|
103
163
|
allDay?: string;
|
|
164
|
+
reschedule?: string;
|
|
165
|
+
pickDate?: string;
|
|
166
|
+
unassigned?: string;
|
|
167
|
+
resource?: string;
|
|
104
168
|
}
|
|
105
169
|
|
|
106
170
|
export interface CalendarProps<Event = Record<string, unknown>> {
|
|
@@ -125,6 +189,19 @@ export interface CalendarProps<Event = Record<string, unknown>> {
|
|
|
125
189
|
hideWeekends?: boolean;
|
|
126
190
|
/** Max chips per day cell in month view before collapsing to "N more". */
|
|
127
191
|
maxEventsPerDay?: number;
|
|
192
|
+
/**
|
|
193
|
+
* Month-cell event token style. `"statusTag"` (default) renders a colored dot +
|
|
194
|
+
* text matching the week/day grid; `"tag"` renders a bordered pill. Both
|
|
195
|
+
* truncate and hold a fixed height as columns narrow.
|
|
196
|
+
*/
|
|
197
|
+
monthEventStyle?: "statusTag" | "tag";
|
|
198
|
+
/**
|
|
199
|
+
* Max characters for a month-cell event label before it's truncated with "…".
|
|
200
|
+
* Defaults to a per-style budget derived from the column width. Labels are
|
|
201
|
+
* truncated up front (not just via the tag's native TruncateString) so every
|
|
202
|
+
* token truncates consistently regardless of the table's column-sizing timing.
|
|
203
|
+
*/
|
|
204
|
+
monthEventMaxChars?: number;
|
|
128
205
|
|
|
129
206
|
// Time grid (week / day views)
|
|
130
207
|
/** First hour row in week/day view (0–23). Default 8. */
|
|
@@ -132,6 +209,36 @@ export interface CalendarProps<Event = Record<string, unknown>> {
|
|
|
132
209
|
/** Last hour row in week/day view (0–23). Default 20. */
|
|
133
210
|
dayEndHour?: number;
|
|
134
211
|
|
|
212
|
+
// Resource / lane view — rows = resources, columns = the focused week's days.
|
|
213
|
+
// The "resource" view only joins the view switcher when `resources` or
|
|
214
|
+
// `resourceField` is provided.
|
|
215
|
+
/** Declared lanes, in display order. Always render, even when empty. */
|
|
216
|
+
resources?: CalendarResource[];
|
|
217
|
+
/** How to read an event's resource id (key on the raw event, or accessor).
|
|
218
|
+
* Ids found in the data but never declared get appended derived lanes. */
|
|
219
|
+
resourceField?: CalendarResourceField<Event>;
|
|
220
|
+
/** `{ [id]: label }` lookup for resources declared without a label and for
|
|
221
|
+
* derived lanes. Falls back to `String(id)`. */
|
|
222
|
+
resourceLabels?: Record<string, ReactNode>;
|
|
223
|
+
/** Append a trailing "Unassigned" lane for events with no resource id
|
|
224
|
+
* (rendered only when non-empty). When false those events are omitted from
|
|
225
|
+
* the resource view. Default `true`. */
|
|
226
|
+
showUnassignedLane?: boolean;
|
|
227
|
+
|
|
228
|
+
// Drag-free reschedule — adds preset buttons + a "Pick date" DateInput to the
|
|
229
|
+
// default event-detail overlay. `true` = "+1 hour" / "+1 day" / "Next week".
|
|
230
|
+
// The Calendar EMITS the shifted range and never mutates its own events.
|
|
231
|
+
rescheduleOptions?: true | CalendarRescheduleOption<Event>[];
|
|
232
|
+
/** Fires when a reschedule affordance is used. `range` has BOTH endpoints
|
|
233
|
+
* shifted (duration preserved, DST-safe), computed from the event's original
|
|
234
|
+
* (`sourceStart`/`sourceEnd`) instants. Persist it and pass updated `events`
|
|
235
|
+
* back in — the Calendar will not move the event on its own. */
|
|
236
|
+
onEventReschedule?: (
|
|
237
|
+
raw: Event,
|
|
238
|
+
range: CalendarRescheduleRange,
|
|
239
|
+
event: CalendarNormalizedEvent<Event>
|
|
240
|
+
) => void;
|
|
241
|
+
|
|
135
242
|
// Timezone — OFF by default: with none of these props set, events render exactly
|
|
136
243
|
// as provided (no conversion, browser-local). Opt in via `timeZone`,
|
|
137
244
|
// `defaultTimeZone`, or `showTimeZoneSelect` and all times, grid placement, and
|
|
@@ -9,7 +9,9 @@ Reusable UI wrappers built on top of HubSpot UI Extensions primitives.
|
|
|
9
9
|
- `AvatarStack` — overlapping circular avatars (letters or image URLs)
|
|
10
10
|
- `Icon` — superset of HubSpot's native `<Icon>`: custom glyphs, any CSS color, pixel sizes
|
|
11
11
|
- `CrmLookupSelect` — CRM-backed `Select` / `MultiSelect` with live, debounced search
|
|
12
|
+
- `CrmRecordPicker` — multi-association record picker: search CRM records, select many, get ids AND records back, optional inline create
|
|
12
13
|
- `CollectionToolbar`, `CollectionFilterControl`, `ActiveFilterChips`, `CollectionSortSelect`, `CollectionCount` — shared search/filter/sort/count primitives used by DataTable, Kanban, Feed, and Calendar
|
|
14
|
+
- `DateRangePicker` — HubSpot-style date filter value control with preset, static-date, rolling, range, and presence operator layouts
|
|
13
15
|
- `SectionHeader` — title + optional description row
|
|
14
16
|
- `KeyValueList` — vertical list of label/value rows
|
|
15
17
|
- `StyledText` — SVG-rendered text with rotation, custom colors, pill backgrounds
|
|
@@ -21,6 +23,7 @@ Plus utilities + constants:
|
|
|
21
23
|
- `ICONS`, `ICON_NAMES`, `NATIVE_ICON_NAME_LIST`, `svgToIconEntry` — the custom icon registry and helpers behind `Icon`
|
|
22
24
|
- `SPINNERS`, `SPINNER_NAMES` — spinner presets and registry
|
|
23
25
|
- `HS_DATE_PRESETS`, `HS_DATE_DIRECTION_LABELS` — HubSpot's native quick-date preset list
|
|
26
|
+
- `presetToRange`, `toHsDateValue`, `compareHsDateValues`, `isValidDateRange`, `DATE_FILTER_OPERATORS`, `DATE_ROLLING_UNIT_OPTIONS`, `DATE_RANGE_CUSTOM_VALUE` — date-filter constants and math behind `DateRangePicker`
|
|
24
27
|
- `HS_FONT_FAMILY`, `HS_TEXT_COLOR`, `HS_SUBTLE_BG`, `HS_MUTED_TEXT`, `HS_NEUTRAL_CHIP` — style constants matching native HubSpot CSS
|
|
25
28
|
|
|
26
29
|
## Purpose
|
|
@@ -38,8 +41,10 @@ import {
|
|
|
38
41
|
AvatarStack,
|
|
39
42
|
Icon,
|
|
40
43
|
CrmLookupSelect,
|
|
44
|
+
CrmRecordPicker,
|
|
41
45
|
CollectionToolbar,
|
|
42
46
|
CollectionSortSelect,
|
|
47
|
+
DateRangePicker,
|
|
43
48
|
SectionHeader,
|
|
44
49
|
KeyValueList,
|
|
45
50
|
StyledText,
|
|
@@ -128,6 +133,76 @@ const activeChips = buildActiveFilterChips(filters, filterValues);
|
|
|
128
133
|
|
|
129
134
|
---
|
|
130
135
|
|
|
136
|
+
## DateRangePicker
|
|
137
|
+
|
|
138
|
+
HubSpot's CRM filter editor changes the value control based on the selected date operator. `DateRangePicker` follows that pattern: `is` renders a quick-preset `Select`, static comparisons render one date input, rolling comparisons render a number plus unit dropdown, `is between` renders two date inputs with a `to` separator, and known / unknown render no value input. Its `onChange` payload preserves the operator so server-side CRM search builders can distinguish preset, static-date, rolling, explicit-range, and presence filters.
|
|
139
|
+
|
|
140
|
+
```jsx
|
|
141
|
+
import { DateRangePicker } from "hs-uix/common-components";
|
|
142
|
+
|
|
143
|
+
const [range, setRange] = useState({
|
|
144
|
+
operator: "InRollingDateRange",
|
|
145
|
+
preset: "today",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
<DateRangePicker
|
|
149
|
+
label="Close date"
|
|
150
|
+
name="close-date"
|
|
151
|
+
value={range}
|
|
152
|
+
onChange={setRange}
|
|
153
|
+
clearable
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Features:
|
|
158
|
+
|
|
159
|
+
- **HubSpot-style operator values.** The built-in operators are `InRollingDateRange` (`is`), `Equal` (`is equal to`), `BeforeDateStaticOrDynamic` (`is before`), `AfterDateStaticOrDynamic` (`is after`), `InRange` (`is between`), `GreaterRolling` (`is more than`), `LessRolling` (`is less than`), `Known` (`is known`), and `NotKnown` (`is unknown`).
|
|
160
|
+
- **Presets can still resolve to dates.** Picking "Last quarter" includes the computed `{ from, to }` bounds in `meta.range` via `presetToRange`.
|
|
161
|
+
- **Compact date inputs by default.** Date inputs use the same no-label treatment as `FilterBuilder` for CRM-filter-style rows. Set `showDateLabels` to `true` to render labels.
|
|
162
|
+
- **Only valid explicit ranges escape.** If an `InRange` edit would make `from > to`, the invalid half is held locally with an error message and `onChange` is NOT called until the user fixes either side.
|
|
163
|
+
- **Controlled or uncontrolled** via `value` / `defaultValue` / `onChange`.
|
|
164
|
+
- **Plain `{ from, to }` compatibility.** Passing the older range shape is treated as `operator: "InRange"`.
|
|
165
|
+
|
|
166
|
+
| Prop | Type | Default | Notes |
|
|
167
|
+
| ---- | ---- | ------- | ----- |
|
|
168
|
+
| `value` | operator value | — | Controlled value. Use `{ operator: "InRollingDateRange", preset }`, `{ operator: "Equal", date }`, `{ operator: "BeforeDateStaticOrDynamic", date }`, `{ operator: "AfterDateStaticOrDynamic", date }`, `{ operator: "GreaterRolling", amount, unit, direction }`, `{ operator: "LessRolling", amount, unit, direction }`, `{ operator: "InRange", from, to }`, `{ operator: "Known" }`, `{ operator: "NotKnown" }`, or legacy `{ from, to }`. |
|
|
169
|
+
| `defaultValue` | operator value | `{ operator: "InRollingDateRange", preset: "today" }` | Initial value for uncontrolled usage. |
|
|
170
|
+
| `onChange` | `(value, meta) => void` | — | Fires with the normalized operator value. `meta` includes `{ operator, field?, preset, range? }`. |
|
|
171
|
+
| `label` | `ReactNode` | — | Group label rendered above the control. |
|
|
172
|
+
| `name` | `string` | `"date-range"` | Base for inner input names. |
|
|
173
|
+
| `field` / `defaultField` | `string` | — | Controlled or uncontrolled selected CRM property when `showFieldSelect` is enabled. |
|
|
174
|
+
| `onFieldChange` | `(field) => void` | — | Fires when the field dropdown changes. |
|
|
175
|
+
| `showFieldSelect` | `boolean` | `false` | Render a CRM property dropdown before the operator and value controls. |
|
|
176
|
+
| `fieldOptions` | array | `[]` | Options for the field dropdown, shaped like `{ label, value }`. |
|
|
177
|
+
| `operator` / `defaultOperator` | date operator | — / `"InRollingDateRange"` | Controlled or uncontrolled operator. |
|
|
178
|
+
| `showOperatorSelect` | `boolean` | `true` | Render the operator dropdown. |
|
|
179
|
+
| `operatorOptions` | array | `DATE_FILTER_OPERATORS` | Override operator labels or available operators. |
|
|
180
|
+
| `presets` | `boolean \| array` | `true` | `true` = `HS_DATE_PRESETS`; `false` = no preset Select; or a custom `{ label, value, getRange? }` array — `value` is a `presetToRange` key, or supply `getRange(now)` for fully custom presets. |
|
|
181
|
+
| `rollingUnitOptions` | array | `DATE_ROLLING_UNIT_OPTIONS` | Options for the rolling amount unit/direction dropdown. |
|
|
182
|
+
| `direction` | `"row" \| "column"` | `"row"` | Layout direction for the operator/value controls. |
|
|
183
|
+
| `clearable` | `boolean` | `false` | Show a Clear link when the current operator has a non-empty value. |
|
|
184
|
+
| `min` / `max` | date object | — | Passed through to both `DateInput`s. |
|
|
185
|
+
| `fromLabel` / `toLabel` / `dateLabel` | `string` | `"Start date"` / `"End date"` / `"Date"` | Date input labels used when `showDateLabels` is true. |
|
|
186
|
+
| `showDateLabels` | `boolean` | `false` | Render DateInput labels visibly instead of the compact no-label CRM-filter layout. |
|
|
187
|
+
| `format` | `string` | `"medium"` | `DateInput` display format. |
|
|
188
|
+
| `presetPlaceholder` | `string` | `"Enter value"` | Placeholder for the preset Select. |
|
|
189
|
+
| `customPresetLabel` | `string` | `"Custom"` | Label for the appended Custom option. |
|
|
190
|
+
| `clearLabel` | `ReactNode` | `"Clear"` | Clear link text. |
|
|
191
|
+
| `invalidRangeMessage` | `string` | `"Start date must be on or before end date"` | Shown on the held invalid input. |
|
|
192
|
+
| `readOnly` | `boolean` | `false` | Pass-through to all inner controls (also hides the Clear link). |
|
|
193
|
+
| `gap` | `string` | `"xs"` row / `"sm"` column | Flex gap between controls. |
|
|
194
|
+
| `gridColumnWidth` | `number` | `260` | Minimum column width for the field-enabled flexible AutoGrid layout. |
|
|
195
|
+
|
|
196
|
+
### Pure helpers (`dateRangePresets.js`)
|
|
197
|
+
|
|
198
|
+
- `presetToRange(presetKey, now?)` — translate an `HS_DATE_PRESETS` key into `{ from, to }` HubSpot date objects. Weeks run Sunday–Saturday; `7d`/`30d`/`90d` are rolling windows ending (and including) today; months/quarters/years are full calendar units. Returns `null` for unknown/`"custom"`/empty keys. Pass a fixed `now` Date for determinism.
|
|
199
|
+
- `toHsDateValue(date)` — JS `Date` → `{ year, month, date }` (or `null`).
|
|
200
|
+
- `compareHsDateValues(a, b)` — sort-style comparator; `null` sides compare as `0`.
|
|
201
|
+
- `isValidDateRange(range)` — `true` when open-ended or `from <= to`.
|
|
202
|
+
- `DATE_RANGE_CUSTOM_VALUE` — the `"custom"` sentinel used by the preset Select.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
131
206
|
## AvatarStack
|
|
132
207
|
|
|
133
208
|
Overlapping circular avatars rendered as a single SVG via `<Image>`. Letters get colored circles with white initials; `http(s):` or `data:image/...` URIs get circular-clipped images. Extras beyond `maxVisible` collapse into a neutral `+N` chip.
|
|
@@ -344,6 +419,69 @@ import { CrmLookupSelect } from "hs-uix/common-components";
|
|
|
344
419
|
|
|
345
420
|
---
|
|
346
421
|
|
|
422
|
+
## CrmRecordPicker
|
|
423
|
+
|
|
424
|
+
The association picker. `CrmLookupSelect` answers "pick ONE record" — `CrmRecordPicker` answers "pick MANY, give me the records back, and let the user create one inline". Use it whenever you're managing a record's associations (contacts on a deal, companies on a ticket) or any selection where you need the full record objects, not just ids.
|
|
425
|
+
|
|
426
|
+
What you get over a hand-rolled `MultiSelect` + `useCrmSearchOptions`:
|
|
427
|
+
|
|
428
|
+
- **Selections never vanish.** A selected record stays visible as an option even when the live search page no longer contains it.
|
|
429
|
+
- **ids AND records.** `onChange(ids, records)` hands back both — no second fetch to resolve what the user picked. `value` accepts ids, records, or a mix.
|
|
430
|
+
- **`max` enforcement.** Picks beyond the cap are rejected (the existing selection is kept), and the create option hides at the cap.
|
|
431
|
+
- **Guarded inline create.** With `allowCreate`, a settled search with no exact label match appends `Create "<term>"`; choosing it awaits `onCreate(term)`, selects the result, and merges it into the options. Double-fires are blocked while the create call is pending.
|
|
432
|
+
|
|
433
|
+
```jsx
|
|
434
|
+
import { CrmRecordPicker } from "hs-uix/common-components";
|
|
435
|
+
|
|
436
|
+
<CrmRecordPicker
|
|
437
|
+
objectType="contact"
|
|
438
|
+
properties={["firstname", "lastname", "email"]}
|
|
439
|
+
label="Associated contacts"
|
|
440
|
+
labelField={(r) => `${r.firstname} ${r.lastname}`}
|
|
441
|
+
descriptionField="email"
|
|
442
|
+
value={contactIds}
|
|
443
|
+
onChange={(ids, records) => {
|
|
444
|
+
setContactIds(ids);
|
|
445
|
+
syncAssociations(records);
|
|
446
|
+
}}
|
|
447
|
+
max={10}
|
|
448
|
+
allowCreate={{
|
|
449
|
+
label: (term) => `Create contact "${term}"`,
|
|
450
|
+
onCreate: async (term) => {
|
|
451
|
+
const created = await hubspot.serverless("createContact", { parameters: { email: term } });
|
|
452
|
+
return created; // record object or its id — both work
|
|
453
|
+
},
|
|
454
|
+
}}
|
|
455
|
+
/>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Single-select mode (`multi={false}`) behaves like `CrmLookupSelect` but keeps this component's richer API: `onChange(id, record)` with a scalar id (or `null` when cleared), plus `allowCreate` — which `CrmLookupSelect` doesn't have.
|
|
459
|
+
|
|
460
|
+
### Props
|
|
461
|
+
|
|
462
|
+
| Prop | Type | Default | Notes |
|
|
463
|
+
| ---- | ---- | ------- | ----- |
|
|
464
|
+
| `objectType` | string | — | CRM object to search (`"contact"`, `"company"`, `"deal"`, or any object type id/name). |
|
|
465
|
+
| `properties` | `string[]` | — | Properties to fetch (drive labels/descriptions). |
|
|
466
|
+
| `labelField` | string \| `(record) => unknown` | — | Option label: dotted property path or accessor. Falls back to `name`, `properties.name`, then `fallbackLabel`. |
|
|
467
|
+
| `descriptionField` | string \| `(record) => unknown` | — | Option description (omitted when empty). |
|
|
468
|
+
| `value` / `defaultValue` | array of ids and/or records (scalar when `multi={false}`) | — | Controlled / uncontrolled selection. Record objects seed the id→record registry. |
|
|
469
|
+
| `onChange` | `(ids, records) => void` | — | Multi: arrays. Single: scalar id (or `null`) + record (or `null`). Never-seen ids come back as `{ objectId: id }` stubs. |
|
|
470
|
+
| `multi` | boolean | `true` | `MultiSelect` vs `Select`. |
|
|
471
|
+
| `max` | number | — | Selection cap. The pick that would exceed it is rejected. |
|
|
472
|
+
| `allowCreate` | `false` \| `{ label?, onCreate }` | `false` | `onCreate: async (term) => recordOrId`. `label` is a string or `(term) => string`; default `Create "<term>"`. |
|
|
473
|
+
| `filterMap` | `(filters, params) => filterGroups` | — | Scope the search with full HubSpot CRM search syntax (e.g. restrict to a pipeline). |
|
|
474
|
+
| `pageLength` | number | `20` | Results fetched per query. |
|
|
475
|
+
| `debounce` / `minSearchLength` | number | `300` / `0` | Search tuning. |
|
|
476
|
+
| `fallbackLabel` | string | `"Untitled record"` | Label for records whose `labelField` resolves empty. |
|
|
477
|
+
| `onSearchChange` | `(query) => void` | — | Observe the live search input. |
|
|
478
|
+
| `format` / `baseConfig` | object | — | Advanced passthroughs to the CRM search config. |
|
|
479
|
+
| `label`, `name`, `placeholder`, `description`, `tooltip`, `required`, `readOnly`, `error`, `validationMessage`, `variant` | — | — | Standard field props forwarded to the native select; any other props are spread through too. |
|
|
480
|
+
|
|
481
|
+
The pure decision logic (option merging, max enforcement, create-option rules, id↔record mapping) is exported from `recordPickerCore.js` — `mergePickerOptions`, `enforceSelectionMax`, `shouldShowCreateOption`, `makeCreateOption`, `splitCreateSelection`, `normalizeRecordSelection`, `mapIdsToRecords`, `getRecordId`, `upsertRecords`, `CREATE_OPTION_VALUE` — if you're building a custom picker UI on the same rules.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
347
485
|
## HS_DATE_PRESETS
|
|
348
486
|
|
|
349
487
|
HubSpot's native quick-date preset list — matches the Create date dropdown on the Deals board. Use as the `options` for a `select` filter on Kanban / DataTable so consumers don't have to retype the list.
|
|
@@ -362,7 +500,7 @@ filters={[
|
|
|
362
500
|
]}
|
|
363
501
|
```
|
|
364
502
|
|
|
365
|
-
The preset values are stable identifiers (`"today"`, `"7d"`, `"this_quarter"`, etc.)
|
|
503
|
+
The preset values are stable identifiers (`"today"`, `"7d"`, `"this_quarter"`, etc.). To translate them into actual date bounds, use `presetToRange(presetKey, now?)` from this module (see [DateRangePicker](#daterangepicker)) — or do it yourself via `filterFn` on the filter config / server-side in `onFilterChange`.
|
|
366
504
|
|
|
367
505
|
Also exports `HS_DATE_DIRECTION_LABELS` (`{ asc: "Ascending", desc: "Descending" }`) for pairing with direction-specific sort UIs.
|
|
368
506
|
|
|
@@ -379,6 +517,7 @@ Raw style tokens used internally by `StyledText` and `AvatarStack` so they match
|
|
|
379
517
|
| `HS_SUBTLE_BG` | `#F5F8FA` | Tag `variant="subtle"` background |
|
|
380
518
|
| `HS_MUTED_TEXT` | `#7C98B6` | Secondary / microcopy gray |
|
|
381
519
|
| `HS_NEUTRAL_CHIP` | `#CBD6E2` | Neutral chip background (`+N` overflow) |
|
|
520
|
+
| `SKELETON_FILL` | `#DFE3EB` | Skeleton placeholder gray |
|
|
382
521
|
| `HS_TAG_SUBTLE_BORDER` | — | Border color for subtle-variant tag pills |
|
|
383
522
|
| `HS_TAG_TEXT_COLOR` | — | Text color inside tag pills |
|
|
384
523
|
| `HS_TAG_FONT_SIZE` | — | Font size (px) for tag pill text |
|
package/src/datatable/README.md
CHANGED
|
@@ -1020,7 +1020,6 @@ These come from HubSpot UI Extensions itself, not DataTable:
|
|
|
1020
1020
|
| No pixel widths | `TableCell` `width` only accepts `"min"`, `"max"`, or `"auto"`. Numeric pixel values are silently ignored by HubSpot. |
|
|
1021
1021
|
| Input alignment | HubSpot input components (Input, NumberInput, CurrencyInput, etc.) ignore parent `text-align` CSS. DataTable strips `align` when inputs are visible so headers and cells stay consistent. |
|
|
1022
1022
|
| No multi-column sort | Only one column can be sorted at a time. |
|
|
1023
|
-
| No row expansion | No expand/collapse for individual row detail views. Row grouping works, but per-row expansion does not. |
|
|
1024
1023
|
| No export | No built-in CSV/Excel export. You'd need to implement this in a serverless function. |
|
|
1025
1024
|
| Validation on select/toggle/checkbox | `editValidate` only shows error UI on text-based inputs (text, number, currency, textarea, stepper). Select, toggle, and checkbox commit immediately and don't show `validationMessage`. |
|
|
1026
1025
|
|
|
@@ -1031,7 +1030,6 @@ These come from HubSpot UI Extensions itself, not DataTable:
|
|
|
1031
1030
|
Planned for future releases:
|
|
1032
1031
|
|
|
1033
1032
|
- Column visibility toggle so users can show/hide columns
|
|
1034
|
-
- Expandable rows with detail content below each row
|
|
1035
1033
|
- Click-to-copy on individual cell values
|
|
1036
1034
|
- Conditional formatting to color-code cells based on value rules
|
|
1037
1035
|
- Per-column filter dropdowns in the header row
|