hs-uix 1.6.4 → 1.7.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 +2 -0
- package/common-components.d.ts +152 -0
- package/dist/common-components.js +1385 -77
- package/dist/common-components.mjs +1438 -82
- package/dist/datatable.js +293 -242
- package/dist/datatable.mjs +209 -159
- package/dist/feed.js +939 -0
- package/dist/feed.mjs +927 -0
- package/dist/form.js +173 -102
- package/dist/form.mjs +173 -102
- package/dist/index.js +3588 -1071
- package/dist/index.mjs +3291 -783
- package/dist/kanban.js +286 -225
- package/dist/kanban.mjs +180 -119
- package/dist/utils.js +2906 -2
- package/dist/utils.mjs +2944 -1
- package/feed.d.ts +1 -0
- package/index.d.ts +51 -2
- package/package.json +17 -4
- package/packages/datatable/README.md +1046 -0
- package/packages/datatable/index.d.ts +246 -0
- package/packages/feed/README.md +224 -0
- package/packages/feed/index.d.ts +261 -0
- package/packages/form/README.md +1229 -0
- package/packages/form/index.d.ts +498 -0
- package/packages/kanban/README.md +707 -0
- package/packages/kanban/index.d.ts +367 -0
- package/utils.d.ts +122 -0
package/dist/utils.mjs
CHANGED
|
@@ -4,6 +4,2938 @@ var sumBy = (items, keyOrFn) => (items || []).reduce((total, item) => {
|
|
|
4
4
|
return total + Number(value || 0);
|
|
5
5
|
}, 0);
|
|
6
6
|
|
|
7
|
+
// src/utils/crmSearchAdapters.js
|
|
8
|
+
import React4, { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
|
|
9
|
+
import { useCrmSearch, Flex as Flex3, Text as Text3 } from "@hubspot/ui-extensions";
|
|
10
|
+
|
|
11
|
+
// packages/datatable/src/DataTable.jsx
|
|
12
|
+
import React, { useState, useMemo, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
13
|
+
|
|
14
|
+
// src/utils/query.js
|
|
15
|
+
import Fuse from "fuse.js";
|
|
16
|
+
var getEmptyFilterValue = (filter) => {
|
|
17
|
+
const type = filter.type || "select";
|
|
18
|
+
if (type === "multiselect") return [];
|
|
19
|
+
if (type === "dateRange") return { from: null, to: null };
|
|
20
|
+
return "";
|
|
21
|
+
};
|
|
22
|
+
var isFilterActive = (filter, value) => {
|
|
23
|
+
const type = filter.type || "select";
|
|
24
|
+
if (type === "multiselect") return Array.isArray(value) && value.length > 0;
|
|
25
|
+
if (type === "dateRange") return value && (value.from || value.to);
|
|
26
|
+
return !!value;
|
|
27
|
+
};
|
|
28
|
+
var formatDateChip = (dateObj) => {
|
|
29
|
+
if (!dateObj) return "";
|
|
30
|
+
const { year, month, date } = dateObj;
|
|
31
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
32
|
+
month: "short",
|
|
33
|
+
day: "numeric",
|
|
34
|
+
year: "numeric"
|
|
35
|
+
}).format(new Date(year, month, date));
|
|
36
|
+
};
|
|
37
|
+
var dateToTimestamp = (dateObj) => {
|
|
38
|
+
if (!dateObj) return null;
|
|
39
|
+
return new Date(dateObj.year, dateObj.month, dateObj.date).getTime();
|
|
40
|
+
};
|
|
41
|
+
var toStableKey = (value) => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
} catch (_error) {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var filterRows = (rows, filters, values) => {
|
|
49
|
+
let result = rows;
|
|
50
|
+
for (const filter of filters || []) {
|
|
51
|
+
const value = values[filter.name];
|
|
52
|
+
if (!isFilterActive(filter, value)) continue;
|
|
53
|
+
const type = filter.type || "select";
|
|
54
|
+
if (filter.filterFn) {
|
|
55
|
+
result = result.filter((row) => filter.filterFn(row, value));
|
|
56
|
+
} else if (type === "multiselect") {
|
|
57
|
+
result = result.filter((row) => value.includes(row[filter.name]));
|
|
58
|
+
} else if (type === "dateRange") {
|
|
59
|
+
const fromTs = dateToTimestamp(value.from);
|
|
60
|
+
const toTs = value.to ? dateToTimestamp(value.to) + 864e5 - 1 : null;
|
|
61
|
+
result = result.filter((row) => {
|
|
62
|
+
const rowTs = new Date(row[filter.name]).getTime();
|
|
63
|
+
if (Number.isNaN(rowTs)) return false;
|
|
64
|
+
if (fromTs && rowTs < fromTs) return false;
|
|
65
|
+
if (toTs && rowTs > toTs) return false;
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
result = result.filter((row) => row[filter.name] === value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
var searchRows = (rows, term, fields, opts = {}) => {
|
|
75
|
+
const { fuzzy = false, fuzzyOptions } = opts;
|
|
76
|
+
const t = String(term ?? "").toLowerCase();
|
|
77
|
+
if (!t || !fields || fields.length === 0) return rows;
|
|
78
|
+
if (fuzzy) {
|
|
79
|
+
const fuse = new Fuse(rows, {
|
|
80
|
+
keys: fields,
|
|
81
|
+
threshold: 0.4,
|
|
82
|
+
distance: 100,
|
|
83
|
+
ignoreLocation: true,
|
|
84
|
+
...fuzzyOptions
|
|
85
|
+
});
|
|
86
|
+
return fuse.search(t).map((r) => r.item);
|
|
87
|
+
}
|
|
88
|
+
return rows.filter(
|
|
89
|
+
(row) => fields.some((field) => {
|
|
90
|
+
const val = row[field];
|
|
91
|
+
return val && String(val).toLowerCase().includes(t);
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/utils/interactionHooks.js
|
|
97
|
+
import { useRef, useEffect, useCallback } from "react";
|
|
98
|
+
import { useDebounce } from "@hubspot/ui-extensions";
|
|
99
|
+
var useDebouncedDispatch = (value, debounceMs, dispatch) => {
|
|
100
|
+
const debounced = useDebounce(value, debounceMs > 0 ? debounceMs : 300);
|
|
101
|
+
const pendingRef = useRef(null);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (debounceMs <= 0) return;
|
|
104
|
+
if (pendingRef.current == null) return;
|
|
105
|
+
if (debounced !== pendingRef.current) return;
|
|
106
|
+
const next = pendingRef.current;
|
|
107
|
+
pendingRef.current = null;
|
|
108
|
+
dispatch(next);
|
|
109
|
+
}, [debounceMs, debounced, dispatch]);
|
|
110
|
+
return useCallback(
|
|
111
|
+
(next) => {
|
|
112
|
+
if (debounceMs > 0) {
|
|
113
|
+
pendingRef.current = next;
|
|
114
|
+
} else {
|
|
115
|
+
pendingRef.current = null;
|
|
116
|
+
dispatch(next);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[debounceMs, dispatch]
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
var useSelectionReset = ({ resetKey, enabled, isControlled, clearSelection }) => {
|
|
123
|
+
const ref = useRef("");
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!enabled || isControlled) {
|
|
126
|
+
ref.current = resetKey;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (ref.current && ref.current !== resetKey) {
|
|
130
|
+
clearSelection();
|
|
131
|
+
}
|
|
132
|
+
ref.current = resetKey;
|
|
133
|
+
}, [resetKey, enabled, isControlled, clearSelection]);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// packages/datatable/src/editValidation.js
|
|
137
|
+
var editValidationError = (result) => {
|
|
138
|
+
if (result === true || result === void 0 || result === null) return null;
|
|
139
|
+
return typeof result === "string" ? result : "Invalid value";
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// packages/datatable/src/DataTable.jsx
|
|
143
|
+
import {
|
|
144
|
+
Box,
|
|
145
|
+
Button,
|
|
146
|
+
Checkbox,
|
|
147
|
+
CurrencyInput,
|
|
148
|
+
DateInput,
|
|
149
|
+
EmptyState,
|
|
150
|
+
ErrorState,
|
|
151
|
+
Flex,
|
|
152
|
+
Icon,
|
|
153
|
+
Input,
|
|
154
|
+
Link,
|
|
155
|
+
MultiSelect,
|
|
156
|
+
NumberInput,
|
|
157
|
+
SearchInput,
|
|
158
|
+
Select,
|
|
159
|
+
StepperInput,
|
|
160
|
+
Table,
|
|
161
|
+
TableBody,
|
|
162
|
+
TableCell,
|
|
163
|
+
TableFooter,
|
|
164
|
+
TableHead,
|
|
165
|
+
TableHeader,
|
|
166
|
+
TableRow,
|
|
167
|
+
Tag,
|
|
168
|
+
Tile,
|
|
169
|
+
Text,
|
|
170
|
+
TextArea,
|
|
171
|
+
TimeInput,
|
|
172
|
+
Toggle,
|
|
173
|
+
Tooltip
|
|
174
|
+
} from "@hubspot/ui-extensions";
|
|
175
|
+
var NARROW_EDIT_TYPES = /* @__PURE__ */ new Set(["checkbox", "toggle"]);
|
|
176
|
+
var DATE_PATTERN = /^\d{4}[-/]\d{2}[-/]\d{2}/;
|
|
177
|
+
var BOOL_VALUES = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "0", "1"]);
|
|
178
|
+
var SORT_DIRECTIONS = /* @__PURE__ */ new Set(["ascending", "descending", "none"]);
|
|
179
|
+
var normalizeSortState = (columns, sort) => {
|
|
180
|
+
const normalized = {};
|
|
181
|
+
columns.forEach((col) => {
|
|
182
|
+
if (col.sortable) normalized[col.field] = "none";
|
|
183
|
+
});
|
|
184
|
+
if (!sort) return normalized;
|
|
185
|
+
if (sort.field && SORT_DIRECTIONS.has(sort.direction) && sort.field in normalized) {
|
|
186
|
+
normalized[sort.field] = sort.direction;
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
Object.keys(normalized).forEach((field) => {
|
|
190
|
+
const direction = sort[field];
|
|
191
|
+
if (SORT_DIRECTIONS.has(direction)) normalized[field] = direction;
|
|
192
|
+
});
|
|
193
|
+
return normalized;
|
|
194
|
+
};
|
|
195
|
+
var serializeSortState = (sortState) => {
|
|
196
|
+
const activeField = Object.keys(sortState).find((field) => sortState[field] !== "none");
|
|
197
|
+
if (!activeField) return null;
|
|
198
|
+
return { field: activeField, direction: sortState[activeField] };
|
|
199
|
+
};
|
|
200
|
+
var computeAutoWidths = (columns, data) => {
|
|
201
|
+
if (!data || data.length === 0) return {};
|
|
202
|
+
const sample = data.slice(0, 50);
|
|
203
|
+
const results = {};
|
|
204
|
+
columns.forEach((col) => {
|
|
205
|
+
if (col.width && col.cellWidth) return;
|
|
206
|
+
const values = sample.map((row) => row[col.field]).filter((v) => v != null);
|
|
207
|
+
const strings = values.map((v) => {
|
|
208
|
+
const s = String(v);
|
|
209
|
+
const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
|
|
210
|
+
return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
|
|
211
|
+
});
|
|
212
|
+
let widthHint = null;
|
|
213
|
+
let cellWidthHint = null;
|
|
214
|
+
if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
|
|
215
|
+
cellWidthHint = "min";
|
|
216
|
+
}
|
|
217
|
+
if (col.truncate === true) {
|
|
218
|
+
cellWidthHint = cellWidthHint || "min";
|
|
219
|
+
}
|
|
220
|
+
if (strings.length > 0) {
|
|
221
|
+
const lengths = strings.map((s) => s.length);
|
|
222
|
+
const maxLen = Math.max(...lengths);
|
|
223
|
+
const uniqueCount = new Set(strings).size;
|
|
224
|
+
if (values.every((v) => typeof v === "boolean") || strings.every((s) => BOOL_VALUES.has(s.toLowerCase()))) {
|
|
225
|
+
widthHint = widthHint || "min";
|
|
226
|
+
cellWidthHint = cellWidthHint || "min";
|
|
227
|
+
} else if (strings.every((s) => DATE_PATTERN.test(s))) {
|
|
228
|
+
widthHint = widthHint || "min";
|
|
229
|
+
cellWidthHint = cellWidthHint || "auto";
|
|
230
|
+
} else if (values.every((v) => typeof v === "number")) {
|
|
231
|
+
widthHint = widthHint || "auto";
|
|
232
|
+
cellWidthHint = cellWidthHint || "auto";
|
|
233
|
+
} else if (uniqueCount <= 5 && maxLen <= 15) {
|
|
234
|
+
widthHint = widthHint || "min";
|
|
235
|
+
cellWidthHint = cellWidthHint || "auto";
|
|
236
|
+
} else {
|
|
237
|
+
widthHint = widthHint || "auto";
|
|
238
|
+
cellWidthHint = cellWidthHint || "auto";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (col.editable && !NARROW_EDIT_TYPES.has(col.editType) && widthHint === "min") {
|
|
242
|
+
widthHint = "auto";
|
|
243
|
+
}
|
|
244
|
+
results[col.field] = {
|
|
245
|
+
width: widthHint || "auto",
|
|
246
|
+
cellWidth: cellWidthHint || "auto"
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
return results;
|
|
250
|
+
};
|
|
251
|
+
var BOOLEAN_SELECT_OPTIONS = [
|
|
252
|
+
{ label: "Yes", value: true },
|
|
253
|
+
{ label: "No", value: false }
|
|
254
|
+
];
|
|
255
|
+
var resolveEditOptions = (col, data) => {
|
|
256
|
+
if (col.editOptions && col.editOptions.length > 0) return col.editOptions;
|
|
257
|
+
const sample = data.find((row) => row[col.field] != null);
|
|
258
|
+
if (sample && typeof sample[col.field] === "boolean") return BOOLEAN_SELECT_OPTIONS;
|
|
259
|
+
return [];
|
|
260
|
+
};
|
|
261
|
+
var DataTable = ({
|
|
262
|
+
// Data
|
|
263
|
+
data,
|
|
264
|
+
columns,
|
|
265
|
+
renderRow,
|
|
266
|
+
// Search
|
|
267
|
+
searchFields = [],
|
|
268
|
+
searchPlaceholder = "Search...",
|
|
269
|
+
fuzzySearch = false,
|
|
270
|
+
// enable fuzzy matching via Fuse.js
|
|
271
|
+
fuzzyOptions,
|
|
272
|
+
// custom Fuse.js options (threshold, distance, etc.)
|
|
273
|
+
showSearch = true,
|
|
274
|
+
// show the SearchInput in the toolbar
|
|
275
|
+
// Filters
|
|
276
|
+
filters = [],
|
|
277
|
+
showFilterBadges = true,
|
|
278
|
+
// show active filter chips/badges
|
|
279
|
+
showClearFiltersButton,
|
|
280
|
+
// show "Clear all" reset button; defaults to showFilterBadges when omitted
|
|
281
|
+
filterInlineLimit = 2,
|
|
282
|
+
// number of filters shown inline before overflow
|
|
283
|
+
// Pagination
|
|
284
|
+
pageSize = 10,
|
|
285
|
+
maxVisiblePageButtons,
|
|
286
|
+
// max page number buttons to show
|
|
287
|
+
showButtonLabels = true,
|
|
288
|
+
// show First/Prev/Next/Last text labels
|
|
289
|
+
showFirstLastButtons,
|
|
290
|
+
// show First/Last page buttons (default: auto when pageCount > 5)
|
|
291
|
+
// Row count
|
|
292
|
+
title,
|
|
293
|
+
// optional title shown as demibold text above the table toolbar
|
|
294
|
+
showRowCount = true,
|
|
295
|
+
// show "X records" / "X of Y records" text
|
|
296
|
+
rowCountBold = false,
|
|
297
|
+
// bold the row count text
|
|
298
|
+
rowCountText,
|
|
299
|
+
// custom formatter: (shownOnPage, totalMatching) => string
|
|
300
|
+
// Table appearance
|
|
301
|
+
bordered = true,
|
|
302
|
+
// show table borders
|
|
303
|
+
flush = true,
|
|
304
|
+
// remove bottom margin
|
|
305
|
+
scrollable = false,
|
|
306
|
+
// allow horizontal overflow with scrollbar
|
|
307
|
+
// Sorting
|
|
308
|
+
defaultSort = {},
|
|
309
|
+
// Grouping
|
|
310
|
+
groupBy,
|
|
311
|
+
// Footer
|
|
312
|
+
footer,
|
|
313
|
+
// Empty state
|
|
314
|
+
emptyTitle,
|
|
315
|
+
emptyMessage,
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
// Server-side mode
|
|
318
|
+
// -----------------------------------------------------------------------
|
|
319
|
+
serverSide = false,
|
|
320
|
+
loading = false,
|
|
321
|
+
// show loading spinner over the table
|
|
322
|
+
error,
|
|
323
|
+
// error message string or boolean — shows ErrorState
|
|
324
|
+
totalCount,
|
|
325
|
+
// server total (server-side only)
|
|
326
|
+
page: externalPage,
|
|
327
|
+
// controlled page (server-side only)
|
|
328
|
+
searchValue,
|
|
329
|
+
// controlled search term (server-side only)
|
|
330
|
+
filterValues: externalFilterValues,
|
|
331
|
+
// controlled filter values (server-side only)
|
|
332
|
+
sort: externalSort,
|
|
333
|
+
// controlled sort state, e.g. { field: "ascending" }
|
|
334
|
+
searchDebounce = 0,
|
|
335
|
+
// ms to debounce onSearchChange callback
|
|
336
|
+
resetPageOnChange = true,
|
|
337
|
+
// auto-reset to page 1 on search/filter/sort change
|
|
338
|
+
onSearchChange,
|
|
339
|
+
// (searchTerm) => void
|
|
340
|
+
onFilterChange,
|
|
341
|
+
// (filterValues) => void
|
|
342
|
+
onSortChange,
|
|
343
|
+
// (field, direction) => void
|
|
344
|
+
onPageChange,
|
|
345
|
+
// (page) => void
|
|
346
|
+
onParamsChange,
|
|
347
|
+
// ({ search, filters, sort, page }) => void
|
|
348
|
+
// -----------------------------------------------------------------------
|
|
349
|
+
// Row selection
|
|
350
|
+
// -----------------------------------------------------------------------
|
|
351
|
+
selectable = false,
|
|
352
|
+
rowIdField = "id",
|
|
353
|
+
// field name used as unique row identifier
|
|
354
|
+
selectedIds: externalSelectedIds,
|
|
355
|
+
// controlled selection — array of row IDs
|
|
356
|
+
onSelectionChange,
|
|
357
|
+
// (selectedIds[]) => void
|
|
358
|
+
onSelectAllRequest,
|
|
359
|
+
// server-side: ({ selectedIds, pageIds, totalCount }) => void
|
|
360
|
+
selectionActions = [],
|
|
361
|
+
// [{ label, onClick(selectedIds[]), icon?, variant? }]
|
|
362
|
+
selectionResetKey,
|
|
363
|
+
// optional key to force clear uncontrolled selection memory
|
|
364
|
+
resetSelectionOnQueryChange = true,
|
|
365
|
+
// clear uncontrolled selection on search/filter/sort changes
|
|
366
|
+
showSelectionBar = true,
|
|
367
|
+
// show the selection action bar when rows are selected
|
|
368
|
+
recordLabel,
|
|
369
|
+
// { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
|
|
370
|
+
// -----------------------------------------------------------------------
|
|
371
|
+
// Row actions
|
|
372
|
+
// -----------------------------------------------------------------------
|
|
373
|
+
rowActions,
|
|
374
|
+
// [{ label, onClick(row), icon?, variant? }] or (row) => actions[]
|
|
375
|
+
hideRowActionsWhenSelectionActive = false,
|
|
376
|
+
// hide row action column while selected-row action bar is visible
|
|
377
|
+
// -----------------------------------------------------------------------
|
|
378
|
+
// Inline editing
|
|
379
|
+
// -----------------------------------------------------------------------
|
|
380
|
+
editMode,
|
|
381
|
+
// "discrete" (click-to-edit) | "inline" (always show inputs)
|
|
382
|
+
editingRowId,
|
|
383
|
+
// controlled — row ID currently in full-row edit mode
|
|
384
|
+
onRowEdit,
|
|
385
|
+
// (row, field, newValue) => void
|
|
386
|
+
onRowEditInput,
|
|
387
|
+
// optional live-input callback: (row, field, inputValue) => void
|
|
388
|
+
onEditStart,
|
|
389
|
+
// (row, field, currentValue) => void — fires when editing begins
|
|
390
|
+
onEditCancel,
|
|
391
|
+
// (row, field) => void — fires when editing is cancelled without commit
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
// Auto-width
|
|
394
|
+
// -----------------------------------------------------------------------
|
|
395
|
+
autoWidth = true,
|
|
396
|
+
// auto-compute column widths from content analysis
|
|
397
|
+
// -----------------------------------------------------------------------
|
|
398
|
+
// Labels / i18n
|
|
399
|
+
// -----------------------------------------------------------------------
|
|
400
|
+
labels,
|
|
401
|
+
// override hardcoded UI strings for i18n
|
|
402
|
+
// -----------------------------------------------------------------------
|
|
403
|
+
// Render overrides (Phase 3 — full replacement escape hatches)
|
|
404
|
+
// -----------------------------------------------------------------------
|
|
405
|
+
renderSelectionBar,
|
|
406
|
+
// ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
|
|
407
|
+
renderEmptyState,
|
|
408
|
+
// ({ title, message }) => ReactNode
|
|
409
|
+
renderLoadingState,
|
|
410
|
+
// ({ label }) => ReactNode
|
|
411
|
+
renderErrorState
|
|
412
|
+
// ({ error, title, message }) => ReactNode
|
|
413
|
+
}) => {
|
|
414
|
+
const initialSortState = useMemo(() => {
|
|
415
|
+
return normalizeSortState(columns, defaultSort);
|
|
416
|
+
}, [columns, defaultSort]);
|
|
417
|
+
const [internalSearchTerm, setInternalSearchTerm] = useState(
|
|
418
|
+
() => serverSide && searchValue != null ? searchValue : ""
|
|
419
|
+
);
|
|
420
|
+
const [internalFilterValues, setInternalFilterValues] = useState(() => {
|
|
421
|
+
const init = {};
|
|
422
|
+
filters.forEach((f) => {
|
|
423
|
+
init[f.name] = getEmptyFilterValue(f);
|
|
424
|
+
});
|
|
425
|
+
return init;
|
|
426
|
+
});
|
|
427
|
+
const [internalSortState, setInternalSortState] = useState(initialSortState);
|
|
428
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
429
|
+
const [showMoreFilters, setShowMoreFilters] = useState(false);
|
|
430
|
+
const lastAppliedSearchRef = useRef2(
|
|
431
|
+
serverSide && searchValue != null ? searchValue : ""
|
|
432
|
+
);
|
|
433
|
+
const searchTerm = serverSide && searchValue != null ? searchValue : internalSearchTerm;
|
|
434
|
+
useEffect2(() => {
|
|
435
|
+
if (!serverSide || searchValue == null) return;
|
|
436
|
+
if (searchValue === lastAppliedSearchRef.current) return;
|
|
437
|
+
lastAppliedSearchRef.current = searchValue;
|
|
438
|
+
setInternalSearchTerm(searchValue);
|
|
439
|
+
}, [serverSide, searchValue]);
|
|
440
|
+
const filterValues = serverSide && externalFilterValues != null ? externalFilterValues : internalFilterValues;
|
|
441
|
+
const externalSortState = useMemo(
|
|
442
|
+
() => normalizeSortState(columns, externalSort),
|
|
443
|
+
[columns, externalSort]
|
|
444
|
+
);
|
|
445
|
+
const sortState = serverSide && externalSort != null ? externalSortState : internalSortState;
|
|
446
|
+
const activePage = serverSide && externalPage != null ? externalPage : currentPage;
|
|
447
|
+
useEffect2(() => {
|
|
448
|
+
if (!serverSide) setCurrentPage(1);
|
|
449
|
+
}, [internalSearchTerm, internalFilterValues, internalSortState, serverSide]);
|
|
450
|
+
const fireSearchCallback = useCallback2((term) => {
|
|
451
|
+
if (serverSide && onSearchChange) onSearchChange(term);
|
|
452
|
+
}, [serverSide, onSearchChange]);
|
|
453
|
+
const fireParamsChange = useCallback2((overrides) => {
|
|
454
|
+
if (!onParamsChange) return;
|
|
455
|
+
const nextSortState = overrides.sort != null ? normalizeSortState(columns, overrides.sort) : sortState;
|
|
456
|
+
onParamsChange({
|
|
457
|
+
search: overrides.search != null ? overrides.search : searchTerm,
|
|
458
|
+
filters: overrides.filters != null ? overrides.filters : filterValues,
|
|
459
|
+
sort: serializeSortState(nextSortState),
|
|
460
|
+
page: overrides.page != null ? overrides.page : activePage
|
|
461
|
+
});
|
|
462
|
+
}, [onParamsChange, columns, searchTerm, filterValues, sortState, activePage]);
|
|
463
|
+
const resetPage = useCallback2(() => {
|
|
464
|
+
if (resetPageOnChange) {
|
|
465
|
+
setCurrentPage(1);
|
|
466
|
+
if (serverSide && onPageChange) onPageChange(1);
|
|
467
|
+
}
|
|
468
|
+
}, [resetPageOnChange, serverSide, onPageChange]);
|
|
469
|
+
const dispatchSearchChange = useCallback2((term) => {
|
|
470
|
+
lastAppliedSearchRef.current = term;
|
|
471
|
+
fireSearchCallback(term);
|
|
472
|
+
fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
|
|
473
|
+
}, [fireSearchCallback, fireParamsChange, resetPageOnChange]);
|
|
474
|
+
const dispatchSearchDebounced = useDebouncedDispatch(
|
|
475
|
+
internalSearchTerm,
|
|
476
|
+
searchDebounce,
|
|
477
|
+
dispatchSearchChange
|
|
478
|
+
);
|
|
479
|
+
const handleSearchChange = useCallback2((term) => {
|
|
480
|
+
setInternalSearchTerm(term);
|
|
481
|
+
resetPage();
|
|
482
|
+
dispatchSearchDebounced(term);
|
|
483
|
+
}, [dispatchSearchDebounced, resetPage]);
|
|
484
|
+
const handleFilterChange = useCallback2((name, value) => {
|
|
485
|
+
const next = { ...filterValues, [name]: value };
|
|
486
|
+
setInternalFilterValues(next);
|
|
487
|
+
if (serverSide && onFilterChange) onFilterChange(next);
|
|
488
|
+
resetPage();
|
|
489
|
+
fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
|
|
490
|
+
}, [filterValues, serverSide, onFilterChange, fireParamsChange, resetPage, resetPageOnChange]);
|
|
491
|
+
const handleSortChange = useCallback2((field) => {
|
|
492
|
+
const current = sortState[field] || "none";
|
|
493
|
+
const nextDirection = current === "none" ? "ascending" : current === "ascending" ? "descending" : "none";
|
|
494
|
+
const reset = {};
|
|
495
|
+
columns.forEach((col) => {
|
|
496
|
+
if (col.sortable) reset[col.field] = "none";
|
|
497
|
+
});
|
|
498
|
+
const next = nextDirection === "none" ? reset : { ...reset, [field]: nextDirection };
|
|
499
|
+
setInternalSortState(next);
|
|
500
|
+
if (serverSide && onSortChange) onSortChange(field, nextDirection);
|
|
501
|
+
resetPage();
|
|
502
|
+
fireParamsChange({ sort: next, page: resetPageOnChange ? 1 : void 0 });
|
|
503
|
+
}, [sortState, columns, serverSide, onSortChange, fireParamsChange, resetPage, resetPageOnChange]);
|
|
504
|
+
const handlePageChange = useCallback2((page) => {
|
|
505
|
+
setCurrentPage(page);
|
|
506
|
+
if (serverSide && onPageChange) onPageChange(page);
|
|
507
|
+
fireParamsChange({ page });
|
|
508
|
+
}, [serverSide, onPageChange, fireParamsChange]);
|
|
509
|
+
const filteredData = useMemo(() => {
|
|
510
|
+
if (serverSide) return data;
|
|
511
|
+
let result = filterRows(data, filters, filterValues);
|
|
512
|
+
if (searchTerm && searchFields.length > 0) {
|
|
513
|
+
result = searchRows(result, searchTerm, searchFields, {
|
|
514
|
+
fuzzy: fuzzySearch,
|
|
515
|
+
fuzzyOptions
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}, [data, filterValues, searchTerm, filters, searchFields, serverSide, fuzzySearch, fuzzyOptions]);
|
|
520
|
+
const sortedData = useMemo(() => {
|
|
521
|
+
if (serverSide) return filteredData;
|
|
522
|
+
const activeField = Object.keys(sortState).find((k) => sortState[k] !== "none");
|
|
523
|
+
if (!activeField) return filteredData;
|
|
524
|
+
const activeCol = columns.find((c) => c.field === activeField);
|
|
525
|
+
const sortOrder = Array.isArray(activeCol == null ? void 0 : activeCol.sortOrder) ? activeCol.sortOrder : null;
|
|
526
|
+
const sortOrderIndex = (val) => {
|
|
527
|
+
const idx = sortOrder.indexOf(val);
|
|
528
|
+
return idx === -1 ? sortOrder.length : idx;
|
|
529
|
+
};
|
|
530
|
+
return [...filteredData].sort((a, b) => {
|
|
531
|
+
const dir = sortState[activeField] === "ascending" ? 1 : -1;
|
|
532
|
+
const aVal = a[activeField];
|
|
533
|
+
const bVal = b[activeField];
|
|
534
|
+
if (typeof (activeCol == null ? void 0 : activeCol.sortComparator) === "function") {
|
|
535
|
+
return dir * activeCol.sortComparator(aVal, bVal, a, b);
|
|
536
|
+
}
|
|
537
|
+
if (sortOrder) {
|
|
538
|
+
const diff = sortOrderIndex(aVal) - sortOrderIndex(bVal);
|
|
539
|
+
if (diff !== 0) return dir * diff;
|
|
540
|
+
}
|
|
541
|
+
if (aVal == null && bVal == null) return 0;
|
|
542
|
+
if (aVal == null) return 1;
|
|
543
|
+
if (bVal == null) return -1;
|
|
544
|
+
if (aVal < bVal) return -dir;
|
|
545
|
+
if (aVal > bVal) return dir;
|
|
546
|
+
return 0;
|
|
547
|
+
});
|
|
548
|
+
}, [filteredData, sortState, serverSide, columns]);
|
|
549
|
+
const groupedData = useMemo(() => {
|
|
550
|
+
if (!groupBy) return null;
|
|
551
|
+
const source = serverSide ? data : sortedData;
|
|
552
|
+
const groups = {};
|
|
553
|
+
source.forEach((row) => {
|
|
554
|
+
const key = row[groupBy.field] ?? "--";
|
|
555
|
+
if (!groups[key]) groups[key] = [];
|
|
556
|
+
groups[key].push(row);
|
|
557
|
+
});
|
|
558
|
+
let groupKeys = Object.keys(groups);
|
|
559
|
+
if (groupBy.sort) {
|
|
560
|
+
if (typeof groupBy.sort === "function") {
|
|
561
|
+
groupKeys.sort(groupBy.sort);
|
|
562
|
+
} else {
|
|
563
|
+
const dir = groupBy.sort === "desc" ? -1 : 1;
|
|
564
|
+
groupKeys.sort((a, b) => a < b ? -dir : a > b ? dir : 0);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return groupKeys.map((key) => ({
|
|
568
|
+
key,
|
|
569
|
+
label: groupBy.label ? groupBy.label(key, groups[key]) : key,
|
|
570
|
+
rows: groups[key]
|
|
571
|
+
}));
|
|
572
|
+
}, [sortedData, data, groupBy, serverSide]);
|
|
573
|
+
const [expandedGroups, setExpandedGroups] = useState(() => {
|
|
574
|
+
if (!groupBy) return /* @__PURE__ */ new Set();
|
|
575
|
+
const defaultExpanded = groupBy.defaultExpanded !== false;
|
|
576
|
+
if (defaultExpanded && groupedData) {
|
|
577
|
+
return new Set(groupedData.map((g) => g.key));
|
|
578
|
+
}
|
|
579
|
+
return /* @__PURE__ */ new Set();
|
|
580
|
+
});
|
|
581
|
+
useEffect2(() => {
|
|
582
|
+
if (!groupedData) return;
|
|
583
|
+
const defaultExpanded = (groupBy == null ? void 0 : groupBy.defaultExpanded) !== false;
|
|
584
|
+
if (defaultExpanded) {
|
|
585
|
+
setExpandedGroups((prev) => {
|
|
586
|
+
const next = new Set(prev);
|
|
587
|
+
groupedData.forEach((g) => next.add(g.key));
|
|
588
|
+
return next;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}, [groupedData, groupBy]);
|
|
592
|
+
const toggleGroup = useCallback2((key) => {
|
|
593
|
+
setExpandedGroups((prev) => {
|
|
594
|
+
const next = new Set(prev);
|
|
595
|
+
if (next.has(key)) next.delete(key);
|
|
596
|
+
else next.add(key);
|
|
597
|
+
return next;
|
|
598
|
+
});
|
|
599
|
+
}, []);
|
|
600
|
+
const datasetRows = useMemo(() => {
|
|
601
|
+
if (!groupedData) return serverSide ? data : sortedData;
|
|
602
|
+
return groupedData.flatMap((group) => group.rows);
|
|
603
|
+
}, [groupedData, sortedData, data, serverSide]);
|
|
604
|
+
const totalItems = serverSide ? totalCount || data.length : datasetRows.length;
|
|
605
|
+
const pageCount = Math.ceil(totalItems / pageSize);
|
|
606
|
+
const paginatedRows = useMemo(() => {
|
|
607
|
+
if (serverSide) return datasetRows;
|
|
608
|
+
return datasetRows.slice(
|
|
609
|
+
(activePage - 1) * pageSize,
|
|
610
|
+
activePage * pageSize
|
|
611
|
+
);
|
|
612
|
+
}, [serverSide, datasetRows, activePage, pageSize]);
|
|
613
|
+
const displayRows = useMemo(() => {
|
|
614
|
+
if (!groupedData) return paginatedRows.map((row) => ({ type: "data", row }));
|
|
615
|
+
const pageRows = new Set(paginatedRows);
|
|
616
|
+
const rows = [];
|
|
617
|
+
groupedData.forEach((group) => {
|
|
618
|
+
const groupPageRows = group.rows.filter((row) => pageRows.has(row));
|
|
619
|
+
if (groupPageRows.length === 0) return;
|
|
620
|
+
rows.push({ type: "group-header", group });
|
|
621
|
+
if (expandedGroups.has(group.key)) {
|
|
622
|
+
groupPageRows.forEach((row) => rows.push({ type: "data", row }));
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
return rows;
|
|
626
|
+
}, [groupedData, paginatedRows, expandedGroups]);
|
|
627
|
+
const footerData = serverSide ? data : filteredData;
|
|
628
|
+
const activeChips = useMemo(() => {
|
|
629
|
+
const chips = [];
|
|
630
|
+
filters.forEach((filter) => {
|
|
631
|
+
const value = filterValues[filter.name];
|
|
632
|
+
if (!isFilterActive(filter, value)) return;
|
|
633
|
+
const type = filter.type || "select";
|
|
634
|
+
const prefix = filter.chipLabel || filter.placeholder || filter.name;
|
|
635
|
+
if (type === "multiselect") {
|
|
636
|
+
const labels2 = value.map((v) => {
|
|
637
|
+
var _a;
|
|
638
|
+
return ((_a = filter.options.find((o) => o.value === v)) == null ? void 0 : _a.label) || v;
|
|
639
|
+
}).join(", ");
|
|
640
|
+
chips.push({ key: filter.name, label: `${prefix}: ${labels2}` });
|
|
641
|
+
} else if (type === "dateRange") {
|
|
642
|
+
const parts = [];
|
|
643
|
+
if (value.from) parts.push(`from ${formatDateChip(value.from)}`);
|
|
644
|
+
if (value.to) parts.push(`to ${formatDateChip(value.to)}`);
|
|
645
|
+
chips.push({ key: filter.name, label: `${prefix}: ${parts.join(" ")}` });
|
|
646
|
+
} else {
|
|
647
|
+
const option = filter.options.find((o) => o.value === value);
|
|
648
|
+
chips.push({ key: filter.name, label: `${prefix}: ${(option == null ? void 0 : option.label) || value}` });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
return chips;
|
|
652
|
+
}, [filterValues, filters]);
|
|
653
|
+
const handleFilterRemove = useCallback2((key) => {
|
|
654
|
+
if (key === "all") {
|
|
655
|
+
const cleared = {};
|
|
656
|
+
filters.forEach((f) => {
|
|
657
|
+
cleared[f.name] = getEmptyFilterValue(f);
|
|
658
|
+
});
|
|
659
|
+
setInternalFilterValues(cleared);
|
|
660
|
+
if (serverSide && onFilterChange) onFilterChange(cleared);
|
|
661
|
+
resetPage();
|
|
662
|
+
fireParamsChange({ filters: cleared, page: resetPageOnChange ? 1 : void 0 });
|
|
663
|
+
} else {
|
|
664
|
+
const filter = filters.find((f) => f.name === key);
|
|
665
|
+
const emptyVal = filter ? getEmptyFilterValue(filter) : "";
|
|
666
|
+
const next = { ...filterValues, [key]: emptyVal };
|
|
667
|
+
setInternalFilterValues(next);
|
|
668
|
+
if (serverSide && onFilterChange) onFilterChange(next);
|
|
669
|
+
resetPage();
|
|
670
|
+
fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
|
|
671
|
+
}
|
|
672
|
+
}, [filters, filterValues, serverSide, onFilterChange, resetPage, fireParamsChange, resetPageOnChange]);
|
|
673
|
+
const displayCount = serverSide ? totalCount || data.length : filteredData.length;
|
|
674
|
+
const totalDataCount = serverSide ? totalCount || data.length : data.length;
|
|
675
|
+
const shownOnPageCount = displayRows.filter((item) => item.type === "data").length;
|
|
676
|
+
const pluralLabel = ((recordLabel == null ? void 0 : recordLabel.plural) || "records").toLowerCase();
|
|
677
|
+
const singularLabel = ((recordLabel == null ? void 0 : recordLabel.singular) || "record").toLowerCase();
|
|
678
|
+
const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
|
|
679
|
+
const resolvedEmptyTitle = emptyTitle || "No results found";
|
|
680
|
+
const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
|
|
681
|
+
const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
|
|
682
|
+
const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
|
|
683
|
+
const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
|
|
684
|
+
const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
|
|
685
|
+
const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
|
|
686
|
+
const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
|
|
687
|
+
const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
|
|
688
|
+
const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
|
|
689
|
+
const resolvedLoadingMessage = (labels == null ? void 0 : labels.loadingMessage) || "This should only take a moment.";
|
|
690
|
+
const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
|
|
691
|
+
const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
|
|
692
|
+
const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
|
|
693
|
+
const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
|
|
694
|
+
const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
|
|
695
|
+
const [internalSelectedIds, setInternalSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
696
|
+
useEffect2(() => {
|
|
697
|
+
if (externalSelectedIds != null) {
|
|
698
|
+
setInternalSelectedIds(new Set(externalSelectedIds));
|
|
699
|
+
}
|
|
700
|
+
}, [externalSelectedIds]);
|
|
701
|
+
const selectionQueryKey = useMemo(() => {
|
|
702
|
+
if (!resetSelectionOnQueryChange) return "";
|
|
703
|
+
return toStableKey({
|
|
704
|
+
search: searchTerm,
|
|
705
|
+
filters: filterValues,
|
|
706
|
+
sort: serializeSortState(sortState)
|
|
707
|
+
});
|
|
708
|
+
}, [searchTerm, filterValues, sortState, resetSelectionOnQueryChange]);
|
|
709
|
+
const combinedSelectionResetKey = useMemo(
|
|
710
|
+
() => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
|
|
711
|
+
[selectionQueryKey, selectionResetKey]
|
|
712
|
+
);
|
|
713
|
+
const clearSelection = useCallback2(() => setInternalSelectedIds(/* @__PURE__ */ new Set()), []);
|
|
714
|
+
useSelectionReset({
|
|
715
|
+
resetKey: combinedSelectionResetKey,
|
|
716
|
+
enabled: selectable,
|
|
717
|
+
isControlled: externalSelectedIds != null,
|
|
718
|
+
clearSelection
|
|
719
|
+
});
|
|
720
|
+
const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
|
|
721
|
+
const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
|
|
722
|
+
const hasToolbarLeft = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton);
|
|
723
|
+
const countInTitleRow = !!title && showToolbarCount && !hasToolbarLeft;
|
|
724
|
+
const countInToolbar = showToolbarCount && !countInTitleRow;
|
|
725
|
+
const hasToolbarContent = hasToolbarLeft || countInToolbar;
|
|
726
|
+
const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
|
|
727
|
+
const applySelection = useCallback2((nextSet) => {
|
|
728
|
+
if (externalSelectedIds == null) {
|
|
729
|
+
setInternalSelectedIds(nextSet);
|
|
730
|
+
}
|
|
731
|
+
if (onSelectionChange) onSelectionChange([...nextSet]);
|
|
732
|
+
}, [externalSelectedIds, onSelectionChange]);
|
|
733
|
+
const pageRowIds = useMemo(() => {
|
|
734
|
+
if (serverSide) {
|
|
735
|
+
return data.map((row) => row[rowIdField]).filter((id) => id != null);
|
|
736
|
+
}
|
|
737
|
+
return displayRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null);
|
|
738
|
+
}, [serverSide, data, displayRows, rowIdField]);
|
|
739
|
+
const allRowIds = useMemo(
|
|
740
|
+
() => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
|
|
741
|
+
[datasetRows, rowIdField]
|
|
742
|
+
);
|
|
743
|
+
const handleSelectRow = useCallback2((rowId, checked) => {
|
|
744
|
+
const next = new Set(selectedIds);
|
|
745
|
+
if (checked) next.add(rowId);
|
|
746
|
+
else next.delete(rowId);
|
|
747
|
+
applySelection(next);
|
|
748
|
+
}, [selectedIds, applySelection]);
|
|
749
|
+
const handleSelectAll = useCallback2((checked) => {
|
|
750
|
+
const next = new Set(selectedIds);
|
|
751
|
+
pageRowIds.forEach((id) => {
|
|
752
|
+
if (checked) next.add(id);
|
|
753
|
+
else next.delete(id);
|
|
754
|
+
});
|
|
755
|
+
applySelection(next);
|
|
756
|
+
}, [selectedIds, pageRowIds, applySelection]);
|
|
757
|
+
const allVisibleSelected = useMemo(() => {
|
|
758
|
+
return pageRowIds.length > 0 && pageRowIds.every((id) => selectedIds.has(id));
|
|
759
|
+
}, [pageRowIds, selectedIds]);
|
|
760
|
+
const handleSelectAllRows = useCallback2(() => {
|
|
761
|
+
const idsToAdd = serverSide ? pageRowIds : allRowIds;
|
|
762
|
+
const next = new Set(selectedIds);
|
|
763
|
+
idsToAdd.forEach((id) => next.add(id));
|
|
764
|
+
applySelection(next);
|
|
765
|
+
if (serverSide && onSelectAllRequest) {
|
|
766
|
+
onSelectAllRequest({
|
|
767
|
+
selectedIds: [...next],
|
|
768
|
+
pageIds: pageRowIds,
|
|
769
|
+
totalCount: totalCount || data.length
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}, [serverSide, pageRowIds, allRowIds, selectedIds, applySelection, onSelectAllRequest, totalCount, data.length]);
|
|
773
|
+
const handleDeselectAll = useCallback2(() => {
|
|
774
|
+
applySelection(/* @__PURE__ */ new Set());
|
|
775
|
+
}, [applySelection]);
|
|
776
|
+
const [editingCell, setEditingCell] = useState(null);
|
|
777
|
+
const [editValue, setEditValue] = useState(null);
|
|
778
|
+
const [editError, setEditError] = useState(null);
|
|
779
|
+
const startEditing = useCallback2((rowId, field, currentValue) => {
|
|
780
|
+
setEditingCell({ rowId, field });
|
|
781
|
+
setEditValue(currentValue);
|
|
782
|
+
setEditError(null);
|
|
783
|
+
if (onEditStart) {
|
|
784
|
+
const row = data.find((r) => r[rowIdField] === rowId);
|
|
785
|
+
if (row) onEditStart(row, field, currentValue);
|
|
786
|
+
}
|
|
787
|
+
}, [onEditStart, data, rowIdField]);
|
|
788
|
+
const commitEdit = useCallback2((row, field, value, options = {}) => {
|
|
789
|
+
const { keepEditing = false } = options;
|
|
790
|
+
const col = columns.find((c) => c.field === field);
|
|
791
|
+
if (col == null ? void 0 : col.editValidate) {
|
|
792
|
+
const err = editValidationError(col.editValidate(value, row));
|
|
793
|
+
if (err) {
|
|
794
|
+
setEditError(err);
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (onRowEdit) onRowEdit(row, field, value);
|
|
799
|
+
if (!keepEditing) {
|
|
800
|
+
setEditingCell(null);
|
|
801
|
+
setEditValue(null);
|
|
802
|
+
} else {
|
|
803
|
+
setEditValue(value);
|
|
804
|
+
}
|
|
805
|
+
setEditError(null);
|
|
806
|
+
return true;
|
|
807
|
+
}, [onRowEdit, columns]);
|
|
808
|
+
const renderEditControl = (col, row) => {
|
|
809
|
+
const type = col.editType || "text";
|
|
810
|
+
const rowId = row[rowIdField];
|
|
811
|
+
const fieldName = `edit-${rowId}-${col.field}`;
|
|
812
|
+
const commit = (val) => commitEdit(row, col.field, val);
|
|
813
|
+
const exitEdit = () => {
|
|
814
|
+
if (editError) return;
|
|
815
|
+
if (onEditCancel) onEditCancel(row, col.field);
|
|
816
|
+
setEditingCell(null);
|
|
817
|
+
setEditValue(null);
|
|
818
|
+
};
|
|
819
|
+
const extra = col.editProps || {};
|
|
820
|
+
const validate = col.editValidate;
|
|
821
|
+
const validationProps = validate && editError ? { error: true, validationMessage: editError } : {};
|
|
822
|
+
const onInputValidate = validate ? (val) => setEditError(editValidationError(validate(val, row))) : void 0;
|
|
823
|
+
const handleInput = (val) => {
|
|
824
|
+
setEditValue(val);
|
|
825
|
+
if (onInputValidate) onInputValidate(val);
|
|
826
|
+
if (onRowEditInput) onRowEditInput(row, col.field, val);
|
|
827
|
+
};
|
|
828
|
+
const maybeExitDatetimeEdit = () => {
|
|
829
|
+
if (typeof document === "undefined") return;
|
|
830
|
+
setTimeout(() => {
|
|
831
|
+
var _a, _b;
|
|
832
|
+
const activeName = (_b = (_a = document.activeElement) == null ? void 0 : _a.getAttribute) == null ? void 0 : _b.call(_a, "name");
|
|
833
|
+
if (activeName !== `${fieldName}-date` && activeName !== `${fieldName}-time`) {
|
|
834
|
+
exitEdit();
|
|
835
|
+
}
|
|
836
|
+
}, 0);
|
|
837
|
+
};
|
|
838
|
+
switch (type) {
|
|
839
|
+
case "textarea":
|
|
840
|
+
return /* @__PURE__ */ React.createElement(TextArea, { ...extra, name: fieldName, label: "", value: editValue ?? "", onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
|
|
841
|
+
case "number":
|
|
842
|
+
return /* @__PURE__ */ React.createElement(NumberInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
|
|
843
|
+
case "currency":
|
|
844
|
+
return /* @__PURE__ */ React.createElement(CurrencyInput, { currencyCode: "USD", ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
|
|
845
|
+
case "stepper":
|
|
846
|
+
return /* @__PURE__ */ React.createElement(StepperInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
|
|
847
|
+
case "select":
|
|
848
|
+
return /* @__PURE__ */ React.createElement(Select, { variant: "transparent", ...extra, name: fieldName, label: "", value: editValue, onChange: commit, options: resolveEditOptions(col, data) });
|
|
849
|
+
case "multiselect":
|
|
850
|
+
return /* @__PURE__ */ React.createElement(MultiSelect, { ...extra, name: fieldName, label: "", value: editValue || [], onChange: commit, options: resolveEditOptions(col, data) });
|
|
851
|
+
case "date":
|
|
852
|
+
return /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit });
|
|
853
|
+
case "time":
|
|
854
|
+
return /* @__PURE__ */ React.createElement(TimeInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit });
|
|
855
|
+
case "datetime":
|
|
856
|
+
return /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: `${fieldName}-date`, label: "", value: editValue == null ? void 0 : editValue.date, onChange: (val) => {
|
|
857
|
+
const next = { ...editValue, date: val };
|
|
858
|
+
handleInput(next);
|
|
859
|
+
commitEdit(row, col.field, next, { keepEditing: true });
|
|
860
|
+
}, onBlur: maybeExitDatetimeEdit }), /* @__PURE__ */ React.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: editValue == null ? void 0 : editValue.time, onChange: (val) => {
|
|
861
|
+
const next = { ...editValue, time: val };
|
|
862
|
+
handleInput(next);
|
|
863
|
+
commitEdit(row, col.field, next, { keepEditing: true });
|
|
864
|
+
}, onBlur: maybeExitDatetimeEdit }));
|
|
865
|
+
case "toggle":
|
|
866
|
+
return /* @__PURE__ */ React.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!editValue, onChange: commit });
|
|
867
|
+
case "checkbox":
|
|
868
|
+
return /* @__PURE__ */ React.createElement(Checkbox, { ...extra, name: fieldName, checked: !!editValue, onChange: commit });
|
|
869
|
+
default:
|
|
870
|
+
return /* @__PURE__ */ React.createElement(Input, { ...extra, name: fieldName, label: "", value: editValue ?? "", onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const resolvedEditMode = editMode || (columns.some((col) => col.editable) ? "discrete" : null);
|
|
874
|
+
const useColumnRendering = selectable || !!resolvedEditMode || editingRowId != null || showRowActionsColumn || !renderRow;
|
|
875
|
+
const autoWidths = useMemo(
|
|
876
|
+
() => autoWidth ? computeAutoWidths(columns, data) : {},
|
|
877
|
+
[columns, data, autoWidth]
|
|
878
|
+
);
|
|
879
|
+
const defaultWidth = scrollable ? "min" : "auto";
|
|
880
|
+
const getHeaderWidth = (col) => {
|
|
881
|
+
var _a;
|
|
882
|
+
return col.width || ((_a = autoWidths[col.field]) == null ? void 0 : _a.width) || defaultWidth;
|
|
883
|
+
};
|
|
884
|
+
const getCellWidth = (col) => {
|
|
885
|
+
var _a;
|
|
886
|
+
return col.cellWidth || col.width || ((_a = autoWidths[col.field]) == null ? void 0 : _a.cellWidth) || defaultWidth;
|
|
887
|
+
};
|
|
888
|
+
const [inlineErrors, setInlineErrors] = useState({});
|
|
889
|
+
const renderInlineControl = (col, row) => {
|
|
890
|
+
const type = col.editType || "text";
|
|
891
|
+
const rowId = row[rowIdField];
|
|
892
|
+
const fieldName = `inline-${rowId}-${col.field}`;
|
|
893
|
+
const cellKey = `${rowId}-${col.field}`;
|
|
894
|
+
const value = row[col.field];
|
|
895
|
+
const validate = col.editValidate;
|
|
896
|
+
const fire = (val) => {
|
|
897
|
+
if (validate) {
|
|
898
|
+
const err = editValidationError(validate(val, row));
|
|
899
|
+
if (err) {
|
|
900
|
+
setInlineErrors((prev) => ({ ...prev, [cellKey]: err }));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
setInlineErrors((prev) => {
|
|
904
|
+
const next = { ...prev };
|
|
905
|
+
delete next[cellKey];
|
|
906
|
+
return next;
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (onRowEdit) onRowEdit(row, col.field, val);
|
|
910
|
+
};
|
|
911
|
+
const extra = col.editProps || {};
|
|
912
|
+
const cellError = inlineErrors[cellKey];
|
|
913
|
+
const validationProps = cellError ? { error: true, validationMessage: cellError } : {};
|
|
914
|
+
const onInputValidate = validate ? (val) => {
|
|
915
|
+
const err = editValidationError(validate(val, row));
|
|
916
|
+
setInlineErrors((prev) => {
|
|
917
|
+
if (err) return { ...prev, [cellKey]: err };
|
|
918
|
+
const next = { ...prev };
|
|
919
|
+
delete next[cellKey];
|
|
920
|
+
return next;
|
|
921
|
+
});
|
|
922
|
+
} : void 0;
|
|
923
|
+
const emitInput = (val) => {
|
|
924
|
+
if (onInputValidate) onInputValidate(val);
|
|
925
|
+
if (onRowEditInput) onRowEditInput(row, col.field, val);
|
|
926
|
+
};
|
|
927
|
+
switch (type) {
|
|
928
|
+
case "textarea":
|
|
929
|
+
return /* @__PURE__ */ React.createElement(TextArea, { ...extra, name: fieldName, label: "", value: value ?? "", onChange: fire, ...validationProps, onInput: emitInput });
|
|
930
|
+
case "number":
|
|
931
|
+
return /* @__PURE__ */ React.createElement(NumberInput, { ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
|
|
932
|
+
case "currency":
|
|
933
|
+
return /* @__PURE__ */ React.createElement(CurrencyInput, { currencyCode: "USD", ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
|
|
934
|
+
case "stepper":
|
|
935
|
+
return /* @__PURE__ */ React.createElement(StepperInput, { ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
|
|
936
|
+
case "select":
|
|
937
|
+
return /* @__PURE__ */ React.createElement(Select, { ...extra, name: fieldName, label: "", value, onChange: fire, options: resolveEditOptions(col, data) });
|
|
938
|
+
case "multiselect":
|
|
939
|
+
return /* @__PURE__ */ React.createElement(MultiSelect, { ...extra, name: fieldName, label: "", value: value || [], onChange: fire, options: resolveEditOptions(col, data) });
|
|
940
|
+
case "date":
|
|
941
|
+
return /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: fieldName, label: "", value, onChange: fire });
|
|
942
|
+
case "time":
|
|
943
|
+
return /* @__PURE__ */ React.createElement(TimeInput, { ...extra, name: fieldName, label: "", value, onChange: fire });
|
|
944
|
+
case "datetime":
|
|
945
|
+
return /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: `${fieldName}-date`, label: "", value: value == null ? void 0 : value.date, onChange: (val) => {
|
|
946
|
+
fire({ ...value, date: val });
|
|
947
|
+
} }), /* @__PURE__ */ React.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: value == null ? void 0 : value.time, onChange: (val) => {
|
|
948
|
+
fire({ ...value, time: val });
|
|
949
|
+
} }));
|
|
950
|
+
case "toggle":
|
|
951
|
+
return /* @__PURE__ */ React.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!value, onChange: fire });
|
|
952
|
+
case "checkbox":
|
|
953
|
+
return /* @__PURE__ */ React.createElement(Checkbox, { ...extra, name: fieldName, checked: !!value, onChange: fire });
|
|
954
|
+
default:
|
|
955
|
+
return /* @__PURE__ */ React.createElement(Input, { ...extra, name: fieldName, label: "", value: value ?? "", onChange: fire, ...validationProps, onInput: emitInput });
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const renderCellContent = (row, col) => {
|
|
959
|
+
const rowId = row[rowIdField];
|
|
960
|
+
if (resolvedEditMode === "inline" && col.editable) {
|
|
961
|
+
return renderInlineControl(col, row);
|
|
962
|
+
}
|
|
963
|
+
if (editingRowId != null && rowId === editingRowId && col.editable) {
|
|
964
|
+
return renderInlineControl(col, row);
|
|
965
|
+
}
|
|
966
|
+
const isEditing = (editingCell == null ? void 0 : editingCell.rowId) === rowId && (editingCell == null ? void 0 : editingCell.field) === col.field;
|
|
967
|
+
if (isEditing && col.editable) return renderEditControl(col, row);
|
|
968
|
+
const rawValue = row[col.field];
|
|
969
|
+
const rawStr = String(rawValue ?? "");
|
|
970
|
+
if (col.truncate && rawStr.length > 0) {
|
|
971
|
+
if (col.truncate === true) {
|
|
972
|
+
if (col.renderCell) {
|
|
973
|
+
const content2 = col.renderCell(rawValue, row);
|
|
974
|
+
if (col.editable) {
|
|
975
|
+
return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
|
|
976
|
+
}
|
|
977
|
+
return content2;
|
|
978
|
+
}
|
|
979
|
+
if (col.editable) {
|
|
980
|
+
return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
|
|
981
|
+
}
|
|
982
|
+
return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, rawStr);
|
|
983
|
+
}
|
|
984
|
+
const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
|
|
985
|
+
if (rawStr.length > maxLen) {
|
|
986
|
+
const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
|
|
987
|
+
const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
|
|
988
|
+
if (col.editable) {
|
|
989
|
+
return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
|
|
990
|
+
}
|
|
991
|
+
return col.renderCell ? content2 : /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, content2 || "--");
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
|
|
995
|
+
const isEmpty = content == null || content === "";
|
|
996
|
+
if (col.editable) {
|
|
997
|
+
return /* @__PURE__ */ React.createElement(
|
|
998
|
+
Link,
|
|
999
|
+
{
|
|
1000
|
+
variant: "dark",
|
|
1001
|
+
onClick: () => startEditing(rowId, col.field, rawValue)
|
|
1002
|
+
},
|
|
1003
|
+
isEmpty ? "--" : content
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
return isEmpty ? "--" : content;
|
|
1007
|
+
};
|
|
1008
|
+
const renderFilterControl2 = (filter) => {
|
|
1009
|
+
const type = filter.type || "select";
|
|
1010
|
+
if (type === "multiselect") {
|
|
1011
|
+
return /* @__PURE__ */ React.createElement(
|
|
1012
|
+
MultiSelect,
|
|
1013
|
+
{
|
|
1014
|
+
key: filter.name,
|
|
1015
|
+
name: `filter-${filter.name}`,
|
|
1016
|
+
label: "",
|
|
1017
|
+
placeholder: filter.placeholder || "All",
|
|
1018
|
+
value: filterValues[filter.name] || [],
|
|
1019
|
+
onChange: (val) => handleFilterChange(filter.name, val),
|
|
1020
|
+
options: filter.options
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (type === "dateRange") {
|
|
1025
|
+
const rangeVal = filterValues[filter.name] || { from: null, to: null };
|
|
1026
|
+
return /* @__PURE__ */ React.createElement(Flex, { key: filter.name, direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React.createElement(
|
|
1027
|
+
DateInput,
|
|
1028
|
+
{
|
|
1029
|
+
name: `filter-${filter.name}-from`,
|
|
1030
|
+
label: "",
|
|
1031
|
+
placeholder: resolvedDateFromLabel,
|
|
1032
|
+
format: "medium",
|
|
1033
|
+
value: rangeVal.from,
|
|
1034
|
+
onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, from: val })
|
|
1035
|
+
}
|
|
1036
|
+
), /* @__PURE__ */ React.createElement(Icon, { name: "dataSync", size: "sm" }), /* @__PURE__ */ React.createElement(
|
|
1037
|
+
DateInput,
|
|
1038
|
+
{
|
|
1039
|
+
size: "sm",
|
|
1040
|
+
name: `filter-${filter.name}-to`,
|
|
1041
|
+
label: "",
|
|
1042
|
+
placeholder: resolvedDateToLabel,
|
|
1043
|
+
format: "medium",
|
|
1044
|
+
value: rangeVal.to,
|
|
1045
|
+
onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, to: val })
|
|
1046
|
+
}
|
|
1047
|
+
));
|
|
1048
|
+
}
|
|
1049
|
+
return /* @__PURE__ */ React.createElement(
|
|
1050
|
+
Select,
|
|
1051
|
+
{
|
|
1052
|
+
key: filter.name,
|
|
1053
|
+
name: `filter-${filter.name}`,
|
|
1054
|
+
variant: "transparent",
|
|
1055
|
+
placeholder: filter.placeholder || "All",
|
|
1056
|
+
value: filterValues[filter.name],
|
|
1057
|
+
onChange: (val) => handleFilterChange(filter.name, val),
|
|
1058
|
+
options: [
|
|
1059
|
+
{ label: filter.placeholder || "All", value: "" },
|
|
1060
|
+
...filter.options
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
};
|
|
1065
|
+
return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, title), countInTitleRow && /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)), hasToolbarContent && /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ React.createElement(
|
|
1066
|
+
SearchInput,
|
|
1067
|
+
{
|
|
1068
|
+
name: "datatable-search",
|
|
1069
|
+
placeholder: searchPlaceholder,
|
|
1070
|
+
value: internalSearchTerm,
|
|
1071
|
+
onChange: handleSearchChange
|
|
1072
|
+
}
|
|
1073
|
+
), filters.slice(0, filterInlineLimit).map(renderFilterControl2), filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(
|
|
1074
|
+
Button,
|
|
1075
|
+
{
|
|
1076
|
+
variant: "transparent",
|
|
1077
|
+
size: "small",
|
|
1078
|
+
onClick: () => setShowMoreFilters((prev) => !prev)
|
|
1079
|
+
},
|
|
1080
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "filter", size: "sm" }),
|
|
1081
|
+
" ",
|
|
1082
|
+
resolvedFiltersButtonLabel
|
|
1083
|
+
)), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl2)), activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton) && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ React.createElement(Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), resolvedShowClearFiltersButton && /* @__PURE__ */ React.createElement(
|
|
1084
|
+
Button,
|
|
1085
|
+
{
|
|
1086
|
+
variant: "transparent",
|
|
1087
|
+
size: "extra-small",
|
|
1088
|
+
onClick: () => handleFilterRemove("all")
|
|
1089
|
+
},
|
|
1090
|
+
resolvedClearAllLabel
|
|
1091
|
+
)))), countInToolbar && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
|
|
1092
|
+
selectedIds,
|
|
1093
|
+
selectedCount: selectedIds.size,
|
|
1094
|
+
displayCount,
|
|
1095
|
+
countLabel,
|
|
1096
|
+
allSelected: selectedIds.size >= (serverSide ? totalCount || data.length : allRowIds.length),
|
|
1097
|
+
onSelectAll: handleSelectAllRows,
|
|
1098
|
+
onDeselectAll: handleDeselectAll,
|
|
1099
|
+
selectionActions
|
|
1100
|
+
}) : /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(Text, { inline: true, format: { fontWeight: "demibold" } }, typeof resolvedSelectedLabel === "function" ? resolvedSelectedLabel(selectedIds.size, countLabel(selectedIds.size)) : resolvedSelectedLabel), selectedIds.size < (serverSide ? totalCount || data.length : allRowIds.length) && /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, typeof resolvedSelectAllLabel === "function" ? resolvedSelectAllLabel(displayCount, countLabel(displayCount)) : resolvedSelectAllLabel), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, resolvedDeselectAllLabel), selectionActions.map((action, i) => /* @__PURE__ */ React.createElement(
|
|
1101
|
+
Button,
|
|
1102
|
+
{
|
|
1103
|
+
key: i,
|
|
1104
|
+
variant: action.variant || "transparent",
|
|
1105
|
+
size: "extra-small",
|
|
1106
|
+
onClick: () => action.onClick([...selectedIds])
|
|
1107
|
+
},
|
|
1108
|
+
action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
|
|
1109
|
+
" ",
|
|
1110
|
+
action.label
|
|
1111
|
+
)))), showRowCount && displayCount > 0 && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : (
|
|
1112
|
+
// Same EmptyState layout as the empty state, just the "building" image
|
|
1113
|
+
// + a loading message — so loading and empty match with no layout shift.
|
|
1114
|
+
/* @__PURE__ */ React.createElement(Tile, null, /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedLoadingLabel, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedLoadingMessage))))
|
|
1115
|
+
) : error ? renderErrorState ? renderErrorState({
|
|
1116
|
+
error,
|
|
1117
|
+
title: typeof error === "string" ? error : resolvedErrorTitle,
|
|
1118
|
+
message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
|
|
1119
|
+
}) : /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ React.createElement(Tile, null, /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage)))) : /* @__PURE__ */ React.createElement(
|
|
1120
|
+
Table,
|
|
1121
|
+
{
|
|
1122
|
+
bordered,
|
|
1123
|
+
flush,
|
|
1124
|
+
paginated: pageCount > 1,
|
|
1125
|
+
page: activePage,
|
|
1126
|
+
pageCount,
|
|
1127
|
+
onPageChange: handlePageChange,
|
|
1128
|
+
showFirstLastButtons: showFirstLastButtons != null ? showFirstLastButtons : pageCount > 5,
|
|
1129
|
+
showButtonLabels,
|
|
1130
|
+
...maxVisiblePageButtons != null ? { maxVisiblePageButtons } : {}
|
|
1131
|
+
},
|
|
1132
|
+
/* @__PURE__ */ React.createElement(TableHead, null, /* @__PURE__ */ React.createElement(TableRow, null, selectable && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" }, /* @__PURE__ */ React.createElement(
|
|
1133
|
+
Checkbox,
|
|
1134
|
+
{
|
|
1135
|
+
name: "datatable-select-all",
|
|
1136
|
+
"aria-label": "Select all rows",
|
|
1137
|
+
checked: allVisibleSelected,
|
|
1138
|
+
onChange: handleSelectAll
|
|
1139
|
+
}
|
|
1140
|
+
)), columns.map((col) => {
|
|
1141
|
+
const headerAlign = resolvedEditMode === "inline" && col.editable ? void 0 : col.align;
|
|
1142
|
+
return /* @__PURE__ */ React.createElement(
|
|
1143
|
+
TableHeader,
|
|
1144
|
+
{
|
|
1145
|
+
key: col.field,
|
|
1146
|
+
width: getHeaderWidth(col),
|
|
1147
|
+
align: headerAlign,
|
|
1148
|
+
sortDirection: col.sortable ? sortState[col.field] || "none" : "never",
|
|
1149
|
+
onSortChange: col.sortable ? () => handleSortChange(col.field) : void 0
|
|
1150
|
+
},
|
|
1151
|
+
col.description ? /* @__PURE__ */ React.createElement(React.Fragment, null, col.label, "\xA0", /* @__PURE__ */ React.createElement(Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ React.createElement(Tooltip, null, col.description) }, /* @__PURE__ */ React.createElement(Icon, { name: "info", screenReaderText: typeof col.description === "string" ? col.description : void 0 }))) : col.label
|
|
1152
|
+
);
|
|
1153
|
+
}), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" }))),
|
|
1154
|
+
/* @__PURE__ */ React.createElement(TableBody, null, displayRows.map(
|
|
1155
|
+
(item, idx) => item.type === "group-header" ? /* @__PURE__ */ React.createElement(TableRow, { key: `group-${item.group.key}` }, selectable && /* @__PURE__ */ React.createElement(TableCell, { width: "min" }), columns.map((col, colIdx) => {
|
|
1156
|
+
var _a, _b, _c;
|
|
1157
|
+
return /* @__PURE__ */ React.createElement(TableCell, { key: col.field, width: getCellWidth(col), align: colIdx === 0 ? void 0 : col.align }, colIdx === 0 ? /* @__PURE__ */ React.createElement(
|
|
1158
|
+
Link,
|
|
1159
|
+
{
|
|
1160
|
+
variant: "dark",
|
|
1161
|
+
onClick: () => toggleGroup(item.group.key)
|
|
1162
|
+
},
|
|
1163
|
+
/* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(Icon, { name: expandedGroups.has(item.group.key) ? "downCarat" : "right" }), /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, item.group.label))
|
|
1164
|
+
) : ((_a = groupBy.aggregations) == null ? void 0 : _a[col.field]) ? groupBy.aggregations[col.field](item.group.rows, item.group.key) : ((_c = (_b = groupBy.groupValues) == null ? void 0 : _b[item.group.key]) == null ? void 0 : _c[col.field]) ?? "");
|
|
1165
|
+
}), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableCell, { width: "min" })) : useColumnRendering ? /* @__PURE__ */ React.createElement(TableRow, { key: item.row[rowIdField] ?? idx }, selectable && /* @__PURE__ */ React.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React.createElement(
|
|
1166
|
+
Checkbox,
|
|
1167
|
+
{
|
|
1168
|
+
name: `select-${item.row[rowIdField]}`,
|
|
1169
|
+
"aria-label": "Select row",
|
|
1170
|
+
checked: selectedIds.has(item.row[rowIdField]),
|
|
1171
|
+
onChange: (checked) => handleSelectRow(item.row[rowIdField], checked)
|
|
1172
|
+
}
|
|
1173
|
+
)), columns.map((col) => {
|
|
1174
|
+
const rowId = item.row[rowIdField];
|
|
1175
|
+
const isDiscreteEditing = resolvedEditMode === "discrete" && (editingCell == null ? void 0 : editingCell.rowId) === rowId && (editingCell == null ? void 0 : editingCell.field) === col.field;
|
|
1176
|
+
const isRowEditing = editingRowId != null && rowId === editingRowId && col.editable;
|
|
1177
|
+
const isShowingInput = isDiscreteEditing || isRowEditing || resolvedEditMode === "inline" && col.editable;
|
|
1178
|
+
const cellAlign = isShowingInput ? void 0 : col.align;
|
|
1179
|
+
return /* @__PURE__ */ React.createElement(TableCell, { key: col.field, width: isDiscreteEditing || isRowEditing ? "auto" : getCellWidth(col), align: cellAlign }, renderCellContent(item.row, col));
|
|
1180
|
+
}), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, (() => {
|
|
1181
|
+
const resolvedRowActions = typeof rowActions === "function" ? rowActions(item.row) : rowActions;
|
|
1182
|
+
const actions = Array.isArray(resolvedRowActions) ? resolvedRowActions : [];
|
|
1183
|
+
return actions.map((action, i) => /* @__PURE__ */ React.createElement(
|
|
1184
|
+
Button,
|
|
1185
|
+
{
|
|
1186
|
+
key: i,
|
|
1187
|
+
variant: action.variant || "transparent",
|
|
1188
|
+
size: "extra-small",
|
|
1189
|
+
onClick: () => action.onClick(item.row)
|
|
1190
|
+
},
|
|
1191
|
+
action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
|
|
1192
|
+
action.label && ` ${action.label}`
|
|
1193
|
+
));
|
|
1194
|
+
})()))) : renderRow(item.row)
|
|
1195
|
+
)),
|
|
1196
|
+
(footer || columns.some((col) => col.footer)) && /* @__PURE__ */ React.createElement(TableFooter, null, typeof footer === "function" ? footer(footerData) : /* @__PURE__ */ React.createElement(TableRow, null, selectable && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" }), columns.map((col) => {
|
|
1197
|
+
const footerDef = col.footer;
|
|
1198
|
+
const content = typeof footerDef === "function" ? footerDef(footerData) : footerDef || "";
|
|
1199
|
+
return /* @__PURE__ */ React.createElement(TableHeader, { key: col.field, align: col.align }, content);
|
|
1200
|
+
}), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" })))
|
|
1201
|
+
));
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// packages/kanban/src/Kanban.jsx
|
|
1205
|
+
import React3, { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
|
|
1206
|
+
|
|
1207
|
+
// src/common-components/StyledText.js
|
|
1208
|
+
import React2 from "react";
|
|
1209
|
+
import { Image, Tag as Tag2 } from "@hubspot/ui-extensions";
|
|
1210
|
+
|
|
1211
|
+
// src/common-components/svgDefaults.js
|
|
1212
|
+
var HS_FONT_FAMILY = '"Lexend Deca", Helvetica, Arial, sans-serif';
|
|
1213
|
+
var HS_TEXT_COLOR = "#33475b";
|
|
1214
|
+
var HS_SUBTLE_BG = "#F5F8FA";
|
|
1215
|
+
var HS_TAG_SUBTLE_BORDER = "#7C98B6";
|
|
1216
|
+
var HS_TAG_TEXT_COLOR = HS_TEXT_COLOR;
|
|
1217
|
+
var HS_TAG_FONT_SIZE = 12;
|
|
1218
|
+
var HS_TAG_LINE_HEIGHT = 22;
|
|
1219
|
+
var HS_TAG_PADDING_X = 8;
|
|
1220
|
+
var HS_TAG_PADDING_Y = 0;
|
|
1221
|
+
var HS_TAG_BORDER_RADIUS = 0;
|
|
1222
|
+
var HS_TAG_BORDER_WIDTH = 1;
|
|
1223
|
+
|
|
1224
|
+
// src/common-components/StyledText.js
|
|
1225
|
+
var VARIANT_PRESETS = {
|
|
1226
|
+
bodytext: { fontSize: 14, lineHeight: 24, fontWeight: 400 },
|
|
1227
|
+
microcopy: { fontSize: 12, lineHeight: 18, fontWeight: 400 }
|
|
1228
|
+
};
|
|
1229
|
+
var WEIGHT_ALIASES = {
|
|
1230
|
+
bold: 700,
|
|
1231
|
+
demibold: 600,
|
|
1232
|
+
regular: 400
|
|
1233
|
+
};
|
|
1234
|
+
var LINE_DECORATION = {
|
|
1235
|
+
strikethrough: "line-through",
|
|
1236
|
+
underline: "underline"
|
|
1237
|
+
};
|
|
1238
|
+
var ORIENTATION_ROTATION = {
|
|
1239
|
+
horizontal: 0,
|
|
1240
|
+
"vertical-up": -90,
|
|
1241
|
+
"vertical-down": 90
|
|
1242
|
+
};
|
|
1243
|
+
var BACKGROUND_PRESETS = {
|
|
1244
|
+
tag: {
|
|
1245
|
+
color: HS_SUBTLE_BG,
|
|
1246
|
+
borderColor: HS_TAG_SUBTLE_BORDER,
|
|
1247
|
+
borderWidth: HS_TAG_BORDER_WIDTH,
|
|
1248
|
+
radius: HS_TAG_BORDER_RADIUS,
|
|
1249
|
+
paddingX: HS_TAG_PADDING_X,
|
|
1250
|
+
paddingY: HS_TAG_PADDING_Y,
|
|
1251
|
+
height: HS_TAG_LINE_HEIGHT,
|
|
1252
|
+
textColor: HS_TAG_TEXT_COLOR,
|
|
1253
|
+
fontSize: HS_TAG_FONT_SIZE,
|
|
1254
|
+
canvasPaddingX: 0,
|
|
1255
|
+
canvasPaddingY: 0
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
var TAG_VARIANTS = {
|
|
1259
|
+
default: {
|
|
1260
|
+
color: HS_SUBTLE_BG,
|
|
1261
|
+
borderColor: HS_TAG_SUBTLE_BORDER,
|
|
1262
|
+
textColor: HS_TAG_TEXT_COLOR
|
|
1263
|
+
},
|
|
1264
|
+
success: {
|
|
1265
|
+
color: "#E5F8F6",
|
|
1266
|
+
borderColor: "#00BDA5",
|
|
1267
|
+
textColor: "#00BDA5"
|
|
1268
|
+
},
|
|
1269
|
+
warning: {
|
|
1270
|
+
color: "#FEF8F0",
|
|
1271
|
+
borderColor: "#F5C26B",
|
|
1272
|
+
textColor: "#D39913"
|
|
1273
|
+
},
|
|
1274
|
+
error: {
|
|
1275
|
+
color: "#FDEDEE",
|
|
1276
|
+
borderColor: "#F2545B",
|
|
1277
|
+
textColor: "#F2545B"
|
|
1278
|
+
},
|
|
1279
|
+
danger: {
|
|
1280
|
+
color: "#FDEDEE",
|
|
1281
|
+
borderColor: "#F2545B",
|
|
1282
|
+
textColor: "#F2545B"
|
|
1283
|
+
},
|
|
1284
|
+
info: {
|
|
1285
|
+
color: "#E5F5F8",
|
|
1286
|
+
borderColor: "#00A4BD",
|
|
1287
|
+
textColor: "#00A4BD"
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var escapeSvgText = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1291
|
+
var applyTextTransform = (text, transform) => {
|
|
1292
|
+
if (!transform || transform === "none") return String(text);
|
|
1293
|
+
const s = String(text);
|
|
1294
|
+
switch (transform) {
|
|
1295
|
+
case "uppercase":
|
|
1296
|
+
return s.toUpperCase();
|
|
1297
|
+
case "lowercase":
|
|
1298
|
+
return s.toLowerCase();
|
|
1299
|
+
case "capitalize":
|
|
1300
|
+
return s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1301
|
+
case "sentenceCase":
|
|
1302
|
+
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
|
1303
|
+
default:
|
|
1304
|
+
return s;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
var estimateTextWidth = (text, fontSize) => Math.max(fontSize, Math.round(String(text).length * fontSize * 0.58));
|
|
1308
|
+
var resolveBackground = (background) => {
|
|
1309
|
+
if (!background) return null;
|
|
1310
|
+
const preset = background.preset ? BACKGROUND_PRESETS[background.preset] : null;
|
|
1311
|
+
const variant = background.preset === "tag" && background.variant ? TAG_VARIANTS[background.variant] || null : null;
|
|
1312
|
+
return {
|
|
1313
|
+
...preset || {},
|
|
1314
|
+
...variant || {},
|
|
1315
|
+
...background
|
|
1316
|
+
};
|
|
1317
|
+
};
|
|
1318
|
+
var buildBackgroundRect = ({ background, x, y, width, height }) => {
|
|
1319
|
+
const radius = (background == null ? void 0 : background.radius) ?? 3;
|
|
1320
|
+
const fill = (background == null ? void 0 : background.color) ?? "transparent";
|
|
1321
|
+
const borderWidth = (background == null ? void 0 : background.borderWidth) ?? 0;
|
|
1322
|
+
const borderColor = background == null ? void 0 : background.borderColor;
|
|
1323
|
+
if (!borderColor || borderWidth <= 0) {
|
|
1324
|
+
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" />`;
|
|
1325
|
+
}
|
|
1326
|
+
const isTagPreset = (background == null ? void 0 : background.preset) === "tag";
|
|
1327
|
+
const fillRect = `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" />`;
|
|
1328
|
+
const strokeInset = borderWidth / 2;
|
|
1329
|
+
const strokeX = x + strokeInset;
|
|
1330
|
+
const strokeY = y + strokeInset;
|
|
1331
|
+
const strokeW = Math.max(0, width - borderWidth);
|
|
1332
|
+
const strokeH = Math.max(0, height - borderWidth);
|
|
1333
|
+
return fillRect + `<rect x="${strokeX}" y="${strokeY}" width="${strokeW}" height="${strokeH}" rx="${Math.max(
|
|
1334
|
+
0,
|
|
1335
|
+
radius - strokeInset
|
|
1336
|
+
)}" fill="none" stroke="${borderColor}" stroke-width="${borderWidth}"${isTagPreset ? ` shape-rendering="crispEdges"` : ""} />`;
|
|
1337
|
+
};
|
|
1338
|
+
var makeStyledTextDataUri = (text, opts = {}) => {
|
|
1339
|
+
const {
|
|
1340
|
+
variant = "bodytext",
|
|
1341
|
+
format = {},
|
|
1342
|
+
orientation = "horizontal",
|
|
1343
|
+
color: colorProp = HS_TEXT_COLOR,
|
|
1344
|
+
fontFamily = HS_FONT_FAMILY,
|
|
1345
|
+
background: backgroundProp = null,
|
|
1346
|
+
paddingX: paddingXProp = 4,
|
|
1347
|
+
paddingY: paddingYProp = 2,
|
|
1348
|
+
width: widthOverride,
|
|
1349
|
+
height: heightOverride,
|
|
1350
|
+
fontSize: fontSizeOverride
|
|
1351
|
+
} = opts;
|
|
1352
|
+
const preset = VARIANT_PRESETS[variant] || VARIANT_PRESETS.bodytext;
|
|
1353
|
+
const background = resolveBackground(backgroundProp);
|
|
1354
|
+
const fontSize = fontSizeOverride ?? (background == null ? void 0 : background.fontSize) ?? preset.fontSize;
|
|
1355
|
+
const rawWeight = format.fontWeight;
|
|
1356
|
+
const fontWeight = rawWeight ? WEIGHT_ALIASES[rawWeight] ?? rawWeight : preset.fontWeight;
|
|
1357
|
+
const fontStyle = format.italic ? "italic" : "normal";
|
|
1358
|
+
const textDecoration = LINE_DECORATION[format.lineDecoration] || "none";
|
|
1359
|
+
const transformed = applyTextTransform(text, format.textTransform);
|
|
1360
|
+
const lineHeight = (background == null ? void 0 : background.height) ?? preset.lineHeight ?? fontSize;
|
|
1361
|
+
const color = (background == null ? void 0 : background.textColor) ?? colorProp;
|
|
1362
|
+
const paddingX = (background == null ? void 0 : background.canvasPaddingX) ?? paddingXProp;
|
|
1363
|
+
const paddingY = (background == null ? void 0 : background.canvasPaddingY) ?? paddingYProp;
|
|
1364
|
+
const rotate = typeof orientation === "number" ? orientation : ORIENTATION_ROTATION[orientation] ?? 0;
|
|
1365
|
+
const textW = estimateTextWidth(transformed, fontSize);
|
|
1366
|
+
let pillW = 0;
|
|
1367
|
+
let pillH = 0;
|
|
1368
|
+
if (background) {
|
|
1369
|
+
const bgPadX = background.paddingX ?? 6;
|
|
1370
|
+
const bgPadY = background.paddingY ?? 3;
|
|
1371
|
+
pillW = textW + bgPadX * 2;
|
|
1372
|
+
pillH = background.height ?? Math.max(lineHeight, fontSize + bgPadY * 2);
|
|
1373
|
+
}
|
|
1374
|
+
const intrinsicW = (background ? pillW : textW) + paddingX * 2;
|
|
1375
|
+
const intrinsicH = (background ? pillH : lineHeight) + paddingY * 2;
|
|
1376
|
+
const isOrthoRotation = rotate === 90 || rotate === -90 || rotate === 270;
|
|
1377
|
+
const canvasW = widthOverride ?? (isOrthoRotation ? intrinsicH : intrinsicW);
|
|
1378
|
+
const canvasH = heightOverride ?? (isOrthoRotation ? intrinsicW : intrinsicH);
|
|
1379
|
+
const cx = canvasW / 2;
|
|
1380
|
+
const cy = canvasH / 2;
|
|
1381
|
+
const rectX = cx - pillW / 2;
|
|
1382
|
+
const rectY = cy - pillH / 2;
|
|
1383
|
+
const group = (background ? buildBackgroundRect({
|
|
1384
|
+
background,
|
|
1385
|
+
x: rectX,
|
|
1386
|
+
y: rectY,
|
|
1387
|
+
width: pillW,
|
|
1388
|
+
height: pillH
|
|
1389
|
+
}) : "") + `<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central" font-family="${fontFamily.replace(/"/g, """)}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" text-decoration="${textDecoration}" fill="${color}">${escapeSvgText(transformed)}</text>`;
|
|
1390
|
+
const wrapped = rotate ? `<g transform="rotate(${rotate} ${cx} ${cy})">${group}</g>` : group;
|
|
1391
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${canvasW}" height="${canvasH}">` + wrapped + `</svg>`;
|
|
1392
|
+
return {
|
|
1393
|
+
src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`,
|
|
1394
|
+
width: canvasW,
|
|
1395
|
+
height: canvasH
|
|
1396
|
+
};
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// packages/kanban/src/Kanban.jsx
|
|
1400
|
+
import {
|
|
1401
|
+
Alert,
|
|
1402
|
+
AutoGrid,
|
|
1403
|
+
Box as Box2,
|
|
1404
|
+
Button as Button2,
|
|
1405
|
+
Checkbox as Checkbox2,
|
|
1406
|
+
DateInput as DateInput2,
|
|
1407
|
+
DescriptionList,
|
|
1408
|
+
DescriptionListItem,
|
|
1409
|
+
Divider,
|
|
1410
|
+
Dropdown,
|
|
1411
|
+
EmptyState as EmptyState2,
|
|
1412
|
+
Flex as Flex2,
|
|
1413
|
+
Icon as Icon2,
|
|
1414
|
+
Image as Image2,
|
|
1415
|
+
Inline,
|
|
1416
|
+
Link as Link2,
|
|
1417
|
+
LoadingSpinner,
|
|
1418
|
+
Modal,
|
|
1419
|
+
ModalBody,
|
|
1420
|
+
MultiSelect as MultiSelect2,
|
|
1421
|
+
SearchInput as SearchInput2,
|
|
1422
|
+
Select as Select2,
|
|
1423
|
+
Statistics,
|
|
1424
|
+
StatisticsItem,
|
|
1425
|
+
StatisticsTrend,
|
|
1426
|
+
Tag as Tag3,
|
|
1427
|
+
Text as Text2,
|
|
1428
|
+
Tile as Tile2
|
|
1429
|
+
} from "@hubspot/ui-extensions";
|
|
1430
|
+
var DEFAULT_DENSITY = "compact";
|
|
1431
|
+
var DEFAULT_MAX_CARDS = 10;
|
|
1432
|
+
var DEFAULT_MAX_EXPANDED = 50;
|
|
1433
|
+
var DEFAULT_COLUMN_WIDTH = 350;
|
|
1434
|
+
var MIN_COLUMN_WIDTH = 350;
|
|
1435
|
+
var DEFAULT_FILTER_INLINE_LIMIT = 4;
|
|
1436
|
+
var DEFAULT_SEARCH_DEBOUNCE = 250;
|
|
1437
|
+
var DEFAULT_TITLE_TRUNCATE = 60;
|
|
1438
|
+
var applyTruncate = (value, truncate, fallback) => {
|
|
1439
|
+
if (truncate === false) return value;
|
|
1440
|
+
if (typeof value !== "string") return value;
|
|
1441
|
+
const limit = typeof truncate === "number" ? truncate : truncate === true ? fallback || DEFAULT_TITLE_TRUNCATE : fallback || DEFAULT_TITLE_TRUNCATE;
|
|
1442
|
+
if (value.length <= limit) return value;
|
|
1443
|
+
return value.slice(0, limit).trimEnd() + "\u2026";
|
|
1444
|
+
};
|
|
1445
|
+
var DEFAULT_LABELS = {
|
|
1446
|
+
search: "Search cards...",
|
|
1447
|
+
// Only the total is surfaced — callers asked for a single headline number
|
|
1448
|
+
// rather than a "loaded / total" fraction. Fall back to the bare label when
|
|
1449
|
+
// no total is known.
|
|
1450
|
+
showMore: (_shown, total) => total ? `Show more (${total})` : "Show more",
|
|
1451
|
+
showLess: "Show less",
|
|
1452
|
+
loadMore: (_loaded, total) => total ? `Load more (${total})` : "Load more",
|
|
1453
|
+
loadingMore: "Loading...",
|
|
1454
|
+
retryLoadMore: "Retry",
|
|
1455
|
+
emptyColumn: "\u2014",
|
|
1456
|
+
emptyTitle: "No cards",
|
|
1457
|
+
emptyMessage: "Nothing matches the current filters.",
|
|
1458
|
+
loading: "Loading board...",
|
|
1459
|
+
loadingMessage: "This should only take a moment.",
|
|
1460
|
+
errorTitle: "Something went wrong.",
|
|
1461
|
+
errorMessage: "An error occurred while loading data.",
|
|
1462
|
+
cardCount: (n) => String(n),
|
|
1463
|
+
moveTo: "Move",
|
|
1464
|
+
clearAll: "Clear all",
|
|
1465
|
+
selectAll: (count, label) => `Select all ${count} ${label}`,
|
|
1466
|
+
deselectAll: "Deselect all",
|
|
1467
|
+
selected: (count, label) => `${count}\xA0${label}\xA0selected`,
|
|
1468
|
+
filtersButton: "Filters",
|
|
1469
|
+
dateFrom: "From",
|
|
1470
|
+
dateTo: "To",
|
|
1471
|
+
sortButton: "Sort",
|
|
1472
|
+
sortAscending: "Ascending",
|
|
1473
|
+
sortDescending: "Descending",
|
|
1474
|
+
metricsButton: "Metrics"
|
|
1475
|
+
};
|
|
1476
|
+
var makeRotatedTagDataUri = (label) => makeStyledTextDataUri(label, {
|
|
1477
|
+
variant: "microcopy",
|
|
1478
|
+
format: { fontWeight: "demibold" },
|
|
1479
|
+
orientation: "vertical-down",
|
|
1480
|
+
background: { preset: "tag" }
|
|
1481
|
+
});
|
|
1482
|
+
var makeRotatedLabelDataUri = (label) => makeStyledTextDataUri(label, {
|
|
1483
|
+
variant: "bodytext",
|
|
1484
|
+
format: { fontWeight: "demibold" },
|
|
1485
|
+
orientation: "vertical-down"
|
|
1486
|
+
});
|
|
1487
|
+
var canStageReceiveRow = (stage, row, canMove) => {
|
|
1488
|
+
if (!stage) return false;
|
|
1489
|
+
if (typeof canMove === "function" && !canMove(row, stage.value)) return false;
|
|
1490
|
+
if (typeof stage.canEnter === "function" && !stage.canEnter(row)) return false;
|
|
1491
|
+
return true;
|
|
1492
|
+
};
|
|
1493
|
+
var isFieldDirectionSortOption = (option) => !!(option && option.field && (option.direction === "asc" || option.direction === "desc"));
|
|
1494
|
+
var resolveDividers = (cardDividers, density) => {
|
|
1495
|
+
if (cardDividers === true) {
|
|
1496
|
+
return { afterTitle: true, afterSubtitle: true, afterBody: true, afterFooter: true };
|
|
1497
|
+
}
|
|
1498
|
+
if (cardDividers === false) {
|
|
1499
|
+
return { afterTitle: false, afterSubtitle: false, afterBody: false, afterFooter: false };
|
|
1500
|
+
}
|
|
1501
|
+
if (cardDividers && typeof cardDividers === "object") {
|
|
1502
|
+
return {
|
|
1503
|
+
afterTitle: cardDividers.afterTitle ?? false,
|
|
1504
|
+
afterSubtitle: cardDividers.afterSubtitle ?? false,
|
|
1505
|
+
afterBody: cardDividers.afterBody ?? false,
|
|
1506
|
+
afterFooter: cardDividers.afterFooter ?? false
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
if (density === "comfortable") {
|
|
1510
|
+
return { afterTitle: true, afterSubtitle: true, afterBody: true, afterFooter: true };
|
|
1511
|
+
}
|
|
1512
|
+
return { afterTitle: false, afterSubtitle: false, afterBody: true, afterFooter: false };
|
|
1513
|
+
};
|
|
1514
|
+
var partitionFields = (cardFields) => {
|
|
1515
|
+
const buckets = { title: null, subtitle: null, meta: [], body: [], footer: [] };
|
|
1516
|
+
for (const field of cardFields || []) {
|
|
1517
|
+
const placement = field.placement || "body";
|
|
1518
|
+
if (placement === "title" && !buckets.title) buckets.title = field;
|
|
1519
|
+
else if (placement === "subtitle" && !buckets.subtitle) buckets.subtitle = field;
|
|
1520
|
+
else if (placement === "meta") buckets.meta.push(field);
|
|
1521
|
+
else if (placement === "footer") buckets.footer.push(field);
|
|
1522
|
+
else buckets.body.push(field);
|
|
1523
|
+
}
|
|
1524
|
+
return buckets;
|
|
1525
|
+
};
|
|
1526
|
+
var resolveFieldValue = (field, row) => {
|
|
1527
|
+
if (!field) return void 0;
|
|
1528
|
+
if (field.field && row && Object.prototype.hasOwnProperty.call(row, field.field)) {
|
|
1529
|
+
return row[field.field];
|
|
1530
|
+
}
|
|
1531
|
+
return void 0;
|
|
1532
|
+
};
|
|
1533
|
+
var resolveHref = (href, row) => {
|
|
1534
|
+
if (!href) return null;
|
|
1535
|
+
if (typeof href === "function") return href(row);
|
|
1536
|
+
return href;
|
|
1537
|
+
};
|
|
1538
|
+
var KanbanCard = ({
|
|
1539
|
+
row,
|
|
1540
|
+
rowId,
|
|
1541
|
+
stage,
|
|
1542
|
+
stages,
|
|
1543
|
+
fields,
|
|
1544
|
+
density,
|
|
1545
|
+
dividers,
|
|
1546
|
+
bodyAs,
|
|
1547
|
+
maxBodyLines,
|
|
1548
|
+
stageControl,
|
|
1549
|
+
stageControlPlacement,
|
|
1550
|
+
canMove,
|
|
1551
|
+
onStageChangeRequest,
|
|
1552
|
+
isChanging,
|
|
1553
|
+
selectable,
|
|
1554
|
+
selected,
|
|
1555
|
+
onToggleSelect,
|
|
1556
|
+
labels
|
|
1557
|
+
}) => {
|
|
1558
|
+
const titleHref = fields.title ? resolveHref(fields.title.href, row) : null;
|
|
1559
|
+
const rawTitleValue = fields.title ? fields.title.render ? fields.title.render(resolveFieldValue(fields.title, row), row) : resolveFieldValue(fields.title, row) : null;
|
|
1560
|
+
const titleValue = fields.title && typeof rawTitleValue === "string" ? applyTruncate(rawTitleValue, fields.title.truncate, DEFAULT_TITLE_TRUNCATE) : rawTitleValue;
|
|
1561
|
+
const titleNode = titleHref ? /* @__PURE__ */ React3.createElement(Link2, { href: titleHref }, titleValue) : /* @__PURE__ */ React3.createElement(Text2, { format: { fontWeight: "demibold" } }, titleValue);
|
|
1562
|
+
const metaNodes = fields.meta.filter((f) => !f.visible || f.visible(row)).map((f) => {
|
|
1563
|
+
const val = resolveFieldValue(f, row);
|
|
1564
|
+
return /* @__PURE__ */ React3.createElement(Text2, { key: f.field || f.label, variant: "microcopy" }, f.render ? f.render(val, row) : val);
|
|
1565
|
+
});
|
|
1566
|
+
const showSubtitle = density === "comfortable" && fields.subtitle;
|
|
1567
|
+
const subtitleNode = showSubtitle ? fields.subtitle.render ? fields.subtitle.render(resolveFieldValue(fields.subtitle, row), row) : resolveFieldValue(fields.subtitle, row) : null;
|
|
1568
|
+
const bodyFields = fields.body.filter((f) => !f.visible || f.visible(row)).slice(0, maxBodyLines);
|
|
1569
|
+
const footerFields = fields.footer.filter((f) => !f.visible || f.visible(row));
|
|
1570
|
+
const footerAlerts = footerFields.slice(0, -1);
|
|
1571
|
+
const footerActionsField = footerFields.length > 0 ? footerFields[footerFields.length - 1] : null;
|
|
1572
|
+
const renderFooterField = (f, idx) => {
|
|
1573
|
+
const val = resolveFieldValue(f, row);
|
|
1574
|
+
const rendered = f.render ? f.render(val, row) : val;
|
|
1575
|
+
const key = f.key || f.field || f.label || `footer-${idx}`;
|
|
1576
|
+
return /* @__PURE__ */ React3.createElement(React3.Fragment, { key }, rendered);
|
|
1577
|
+
};
|
|
1578
|
+
const stageControlNode = stageControl === "none" ? null : /* @__PURE__ */ React3.createElement(
|
|
1579
|
+
StageControl,
|
|
1580
|
+
{
|
|
1581
|
+
row,
|
|
1582
|
+
rowId,
|
|
1583
|
+
currentStage: stage,
|
|
1584
|
+
stages,
|
|
1585
|
+
canMove,
|
|
1586
|
+
isChanging,
|
|
1587
|
+
mode: stageControl,
|
|
1588
|
+
onStageChangeRequest,
|
|
1589
|
+
labels
|
|
1590
|
+
}
|
|
1591
|
+
);
|
|
1592
|
+
const titleRow = /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "between", align: "center", gap: "sm" }, /* @__PURE__ */ React3.createElement(Box2, { flex: 1 }, titleNode), selectable ? /* @__PURE__ */ React3.createElement(
|
|
1593
|
+
Checkbox2,
|
|
1594
|
+
{
|
|
1595
|
+
name: `kanban-select-${rowId}`,
|
|
1596
|
+
checked: selected,
|
|
1597
|
+
onChange: () => onToggleSelect(rowId)
|
|
1598
|
+
}
|
|
1599
|
+
) : null);
|
|
1600
|
+
const metaRow = metaNodes.length === 0 ? null : /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "end", align: "center", gap: "xs" }, metaNodes);
|
|
1601
|
+
const bodyRow = bodyFields.length === 0 ? null : bodyAs === "descriptionList" ? /* @__PURE__ */ React3.createElement(DescriptionList, { direction: "row" }, bodyFields.map((f, idx) => {
|
|
1602
|
+
const val = resolveFieldValue(f, row);
|
|
1603
|
+
const rendered = f.render ? f.render(val, row) : val ?? "\u2014";
|
|
1604
|
+
const key = f.key || f.field || f.label || `body-${idx}`;
|
|
1605
|
+
return /* @__PURE__ */ React3.createElement(DescriptionListItem, { key, label: f.label || "" }, rendered);
|
|
1606
|
+
})) : /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "flush" }, bodyFields.map((f, idx) => {
|
|
1607
|
+
const val = resolveFieldValue(f, row);
|
|
1608
|
+
const rendered = f.render ? f.render(val, row) : val ?? "\u2014";
|
|
1609
|
+
const key = f.key || f.field || f.label || `body-${idx}`;
|
|
1610
|
+
return /* @__PURE__ */ React3.createElement(Text2, { key, variant: "microcopy" }, f.label ? /* @__PURE__ */ React3.createElement(Text2, { inline: true, variant: "microcopy" }, `${f.label}: `) : null, rendered);
|
|
1611
|
+
}));
|
|
1612
|
+
const footerAlertsNode = footerAlerts.length === 0 ? null : /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, footerAlerts.map((f, idx) => renderFooterField(f, idx)));
|
|
1613
|
+
const footerActionsNode = footerActionsField ? renderFooterField(footerActionsField, footerFields.length - 1) : null;
|
|
1614
|
+
const inlineStageControl = stageControlPlacement === "inline" ? stageControlNode : null;
|
|
1615
|
+
const separateRowStageControl = stageControlPlacement === "separateRow" ? stageControlNode : null;
|
|
1616
|
+
const footerMainRow = inlineStageControl || footerActionsNode ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "between", align: "start", gap: "sm" }, inlineStageControl ? /* @__PURE__ */ React3.createElement(Box2, { alignSelf: "center" }, inlineStageControl) : /* @__PURE__ */ React3.createElement(Box2, null), footerActionsNode ? /* @__PURE__ */ React3.createElement(Box2, { flex: 1 }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "end", align: "start" }, footerActionsNode)) : null) : null;
|
|
1617
|
+
const footerRow = !footerAlertsNode && !footerMainRow ? null : /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, footerAlertsNode, footerMainRow);
|
|
1618
|
+
return /* @__PURE__ */ React3.createElement(Tile2, { compact: density === "compact" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: density === "compact" ? "xs" : "sm" }, titleRow, dividers.afterTitle && (metaRow || bodyRow || footerRow || separateRowStageControl) ? /* @__PURE__ */ React3.createElement(Divider, null) : null, subtitleNode ? /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy" }, subtitleNode) : null, dividers.afterSubtitle && subtitleNode && (metaRow || bodyRow || footerRow || separateRowStageControl) ? /* @__PURE__ */ React3.createElement(Divider, null) : null, metaRow, bodyRow, dividers.afterBody && bodyRow && (footerRow || separateRowStageControl) ? /* @__PURE__ */ React3.createElement(Divider, null) : null, footerRow, dividers.afterFooter && footerRow && separateRowStageControl ? /* @__PURE__ */ React3.createElement(Divider, null) : null, separateRowStageControl));
|
|
1619
|
+
};
|
|
1620
|
+
var StageControl = ({
|
|
1621
|
+
row,
|
|
1622
|
+
rowId,
|
|
1623
|
+
currentStage,
|
|
1624
|
+
stages,
|
|
1625
|
+
canMove,
|
|
1626
|
+
isChanging,
|
|
1627
|
+
mode,
|
|
1628
|
+
onStageChangeRequest,
|
|
1629
|
+
labels
|
|
1630
|
+
}) => {
|
|
1631
|
+
if (isChanging) {
|
|
1632
|
+
return /* @__PURE__ */ React3.createElement(LoadingSpinner, { size: "xs" });
|
|
1633
|
+
}
|
|
1634
|
+
const availableStages = (stages || []).filter(
|
|
1635
|
+
(stage) => stage.value === currentStage.value || canStageReceiveRow(stage, row, canMove)
|
|
1636
|
+
);
|
|
1637
|
+
if (mode === "menu") {
|
|
1638
|
+
const targetStages = availableStages.filter((stage) => stage.value !== currentStage.value);
|
|
1639
|
+
if (targetStages.length === 0) {
|
|
1640
|
+
return /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "extra-small", disabled: true }, labels.moveTo);
|
|
1641
|
+
}
|
|
1642
|
+
return /* @__PURE__ */ React3.createElement(
|
|
1643
|
+
Dropdown,
|
|
1644
|
+
{
|
|
1645
|
+
variant: "transparent",
|
|
1646
|
+
buttonText: labels.moveTo,
|
|
1647
|
+
buttonSize: "xs"
|
|
1648
|
+
},
|
|
1649
|
+
targetStages.map((stage) => /* @__PURE__ */ React3.createElement(
|
|
1650
|
+
Dropdown.ButtonItem,
|
|
1651
|
+
{
|
|
1652
|
+
key: stage.value,
|
|
1653
|
+
onClick: () => onStageChangeRequest(row, stage.value, currentStage.value)
|
|
1654
|
+
},
|
|
1655
|
+
stage.shortLabel || stage.label
|
|
1656
|
+
))
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
return /* @__PURE__ */ React3.createElement(
|
|
1660
|
+
Select2,
|
|
1661
|
+
{
|
|
1662
|
+
name: `stage-${rowId}`,
|
|
1663
|
+
label: "",
|
|
1664
|
+
value: currentStage.value,
|
|
1665
|
+
onChange: (val) => {
|
|
1666
|
+
if (val !== currentStage.value) onStageChangeRequest(row, val, currentStage.value);
|
|
1667
|
+
},
|
|
1668
|
+
options: availableStages.map((stage) => ({
|
|
1669
|
+
label: stage.shortLabel || stage.label,
|
|
1670
|
+
value: stage.value
|
|
1671
|
+
}))
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1674
|
+
};
|
|
1675
|
+
var KanbanColumn = ({
|
|
1676
|
+
stage,
|
|
1677
|
+
rows,
|
|
1678
|
+
bucketCount,
|
|
1679
|
+
totalCount,
|
|
1680
|
+
hasMore,
|
|
1681
|
+
loading,
|
|
1682
|
+
error,
|
|
1683
|
+
onLoadMore,
|
|
1684
|
+
expanded,
|
|
1685
|
+
onToggleExpanded,
|
|
1686
|
+
collapsed,
|
|
1687
|
+
onToggleCollapsed,
|
|
1688
|
+
columnFooter,
|
|
1689
|
+
countDisplay,
|
|
1690
|
+
labels,
|
|
1691
|
+
children
|
|
1692
|
+
}) => {
|
|
1693
|
+
const countLabel = labels.cardCount(totalCount != null ? totalCount : bucketCount);
|
|
1694
|
+
const countNode = countDisplay === "text" ? /* @__PURE__ */ React3.createElement(Text2, { format: { fontWeight: "demibold" } }, countLabel) : countDisplay === "none" ? null : /* @__PURE__ */ React3.createElement(Tag3, { variant: "default" }, countLabel);
|
|
1695
|
+
if (collapsed) {
|
|
1696
|
+
const rotated = makeRotatedLabelDataUri(stage.label);
|
|
1697
|
+
const rotatedCount = countDisplay === "none" ? null : makeRotatedTagDataUri(countLabel);
|
|
1698
|
+
const stageIdentifier = stage.icon ? /* @__PURE__ */ React3.createElement(Icon2, { name: stage.icon, size: "sm", screenReaderText: stage.label }) : /* @__PURE__ */ React3.createElement(
|
|
1699
|
+
Image2,
|
|
1700
|
+
{
|
|
1701
|
+
src: rotated.src,
|
|
1702
|
+
width: rotated.width,
|
|
1703
|
+
height: rotated.height,
|
|
1704
|
+
alt: stage.label
|
|
1705
|
+
}
|
|
1706
|
+
);
|
|
1707
|
+
return /* @__PURE__ */ React3.createElement(Tile2, { compact: true }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs", align: "center" }, /* @__PURE__ */ React3.createElement(
|
|
1708
|
+
Button2,
|
|
1709
|
+
{
|
|
1710
|
+
variant: "transparent",
|
|
1711
|
+
size: "sm",
|
|
1712
|
+
onClick: onToggleCollapsed,
|
|
1713
|
+
tooltip: `Expand ${stage.label}`
|
|
1714
|
+
},
|
|
1715
|
+
/* @__PURE__ */ React3.createElement(Icon2, { name: "right", size: "sm", screenReaderText: `Expand ${stage.label}` })
|
|
1716
|
+
), stageIdentifier, rotatedCount ? /* @__PURE__ */ React3.createElement(
|
|
1717
|
+
Image2,
|
|
1718
|
+
{
|
|
1719
|
+
src: rotatedCount.src,
|
|
1720
|
+
width: rotatedCount.width,
|
|
1721
|
+
height: rotatedCount.height,
|
|
1722
|
+
alt: `${bucketCount} items`
|
|
1723
|
+
}
|
|
1724
|
+
) : null));
|
|
1725
|
+
}
|
|
1726
|
+
const footerContent = stage.footer ? stage.footer(rows) : columnFooter ? columnFooter(rows, stage) : null;
|
|
1727
|
+
return /* @__PURE__ */ React3.createElement(Tile2, { compact: true }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React3.createElement(Text2, { format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), countNode, loading ? /* @__PURE__ */ React3.createElement(LoadingSpinner, { size: "xs" }) : null), /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "sm", onClick: onToggleCollapsed, tooltip: "Collapse" }, /* @__PURE__ */ React3.createElement(Icon2, { name: "left", size: "sm", screenReaderText: `Collapse ${stage.label}` }))), footerContent ? /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy" }, footerContent) : null, /* @__PURE__ */ React3.createElement(Divider, null), children, error ? /* @__PURE__ */ React3.createElement(Alert, { variant: "danger", title: labels.errorTitle }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", gap: "xs", align: "center" }, /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy" }, error), onLoadMore ? /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "xs", onClick: () => onLoadMore(stage.value) }, labels.retryLoadMore) : null)) : null, !error && hasMore && onLoadMore && !loading && bucketCount > 0 ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "center" }, /* @__PURE__ */ React3.createElement(Link2, { onClick: () => onLoadMore(stage.value) }, labels.loadMore(bucketCount, totalCount))) : null, !error && loading && hasMore ? /* @__PURE__ */ React3.createElement(LoadingSpinner, { size: "sm", layout: "centered", label: labels.loadingMore }) : null, !error && !hasMore && bucketCount > rows.length ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "center" }, /* @__PURE__ */ React3.createElement(Link2, { onClick: onToggleExpanded }, expanded ? labels.showLess : labels.showMore(rows.length, bucketCount))) : null, rows.length === 0 && bucketCount === 0 && !loading ? /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn) : null));
|
|
1728
|
+
};
|
|
1729
|
+
var SortModalBody = ({ sortOptions, sortValue, onSortChange, labels }) => {
|
|
1730
|
+
const hasFieldDirection = Array.isArray(sortOptions) && sortOptions.length > 0 && sortOptions.every(isFieldDirectionSortOption);
|
|
1731
|
+
if (!hasFieldDirection) {
|
|
1732
|
+
return /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, sortOptions.map((opt) => /* @__PURE__ */ React3.createElement(
|
|
1733
|
+
Button2,
|
|
1734
|
+
{
|
|
1735
|
+
key: opt.value,
|
|
1736
|
+
variant: sortValue === opt.value ? "primary" : "secondary",
|
|
1737
|
+
onClick: () => onSortChange(opt.value)
|
|
1738
|
+
},
|
|
1739
|
+
opt.label
|
|
1740
|
+
)));
|
|
1741
|
+
}
|
|
1742
|
+
const currentOption = sortOptions.find((o) => o.value === sortValue) || sortOptions[0];
|
|
1743
|
+
const currentField = currentOption.field;
|
|
1744
|
+
const currentDirection = currentOption.direction;
|
|
1745
|
+
const uniqueFields = [];
|
|
1746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1747
|
+
for (const opt of sortOptions) {
|
|
1748
|
+
if (!seen.has(opt.field)) {
|
|
1749
|
+
seen.add(opt.field);
|
|
1750
|
+
uniqueFields.push({ value: opt.field, label: opt.fieldLabel || opt.field });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const fieldOpts = sortOptions.filter((o) => o.field === currentField);
|
|
1754
|
+
const ascOption = fieldOpts.find((o) => o.direction === "asc");
|
|
1755
|
+
const descOption = fieldOpts.find((o) => o.direction === "desc");
|
|
1756
|
+
const handleFieldChange = (newField) => {
|
|
1757
|
+
const next = sortOptions.find((o) => o.field === newField && o.direction === currentDirection) || sortOptions.find((o) => o.field === newField && o.direction === "desc") || sortOptions.find((o) => o.field === newField);
|
|
1758
|
+
if (next) onSortChange(next.value);
|
|
1759
|
+
};
|
|
1760
|
+
return /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "small" }, /* @__PURE__ */ React3.createElement(
|
|
1761
|
+
Select2,
|
|
1762
|
+
{
|
|
1763
|
+
name: "kanban-sort-field",
|
|
1764
|
+
label: "",
|
|
1765
|
+
value: currentField,
|
|
1766
|
+
onChange: handleFieldChange,
|
|
1767
|
+
options: uniqueFields
|
|
1768
|
+
}
|
|
1769
|
+
), /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "flush" }, descOption ? /* @__PURE__ */ React3.createElement(
|
|
1770
|
+
Button2,
|
|
1771
|
+
{
|
|
1772
|
+
variant: currentDirection === "desc" ? "primary" : "secondary",
|
|
1773
|
+
onClick: () => onSortChange(descOption.value)
|
|
1774
|
+
},
|
|
1775
|
+
labels.sortDescending
|
|
1776
|
+
) : null, ascOption ? /* @__PURE__ */ React3.createElement(
|
|
1777
|
+
Button2,
|
|
1778
|
+
{
|
|
1779
|
+
variant: currentDirection === "asc" ? "primary" : "secondary",
|
|
1780
|
+
onClick: () => onSortChange(ascOption.value)
|
|
1781
|
+
},
|
|
1782
|
+
labels.sortAscending
|
|
1783
|
+
) : null));
|
|
1784
|
+
};
|
|
1785
|
+
var renderFilterControl = ({ filter, value, onChange, labels }) => {
|
|
1786
|
+
const type = filter.type || "select";
|
|
1787
|
+
if (type === "multiselect") {
|
|
1788
|
+
return /* @__PURE__ */ React3.createElement(
|
|
1789
|
+
MultiSelect2,
|
|
1790
|
+
{
|
|
1791
|
+
key: filter.name,
|
|
1792
|
+
name: `kanban-filter-${filter.name}`,
|
|
1793
|
+
label: "",
|
|
1794
|
+
placeholder: filter.placeholder || "All",
|
|
1795
|
+
value: value || [],
|
|
1796
|
+
onChange: (val) => onChange(filter.name, val),
|
|
1797
|
+
options: filter.options
|
|
1798
|
+
}
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
if (type === "dateRange") {
|
|
1802
|
+
const rangeVal = value || { from: null, to: null };
|
|
1803
|
+
return /* @__PURE__ */ React3.createElement(Flex2, { key: filter.name, direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React3.createElement(
|
|
1804
|
+
DateInput2,
|
|
1805
|
+
{
|
|
1806
|
+
size: "sm",
|
|
1807
|
+
name: `kanban-filter-${filter.name}-from`,
|
|
1808
|
+
label: "",
|
|
1809
|
+
placeholder: labels.dateFrom,
|
|
1810
|
+
format: "medium",
|
|
1811
|
+
value: rangeVal.from,
|
|
1812
|
+
onChange: (val) => onChange(filter.name, { ...rangeVal, from: val })
|
|
1813
|
+
}
|
|
1814
|
+
), /* @__PURE__ */ React3.createElement(Icon2, { name: "dataSync", size: "sm" }), /* @__PURE__ */ React3.createElement(
|
|
1815
|
+
DateInput2,
|
|
1816
|
+
{
|
|
1817
|
+
size: "sm",
|
|
1818
|
+
name: `kanban-filter-${filter.name}-to`,
|
|
1819
|
+
label: "",
|
|
1820
|
+
placeholder: labels.dateTo,
|
|
1821
|
+
format: "medium",
|
|
1822
|
+
value: rangeVal.to,
|
|
1823
|
+
onChange: (val) => onChange(filter.name, { ...rangeVal, to: val })
|
|
1824
|
+
}
|
|
1825
|
+
));
|
|
1826
|
+
}
|
|
1827
|
+
return /* @__PURE__ */ React3.createElement(
|
|
1828
|
+
Select2,
|
|
1829
|
+
{
|
|
1830
|
+
key: filter.name,
|
|
1831
|
+
name: `kanban-filter-${filter.name}`,
|
|
1832
|
+
variant: "transparent",
|
|
1833
|
+
placeholder: filter.placeholder || "All",
|
|
1834
|
+
value,
|
|
1835
|
+
onChange: (val) => onChange(filter.name, val),
|
|
1836
|
+
options: [
|
|
1837
|
+
{ label: filter.placeholder || "All", value: "" },
|
|
1838
|
+
...filter.options
|
|
1839
|
+
]
|
|
1840
|
+
}
|
|
1841
|
+
);
|
|
1842
|
+
};
|
|
1843
|
+
var renderMetricsPanel = (metrics) => {
|
|
1844
|
+
if (!metrics) return null;
|
|
1845
|
+
if (!Array.isArray(metrics)) return metrics;
|
|
1846
|
+
if (metrics.length === 0) return null;
|
|
1847
|
+
return /* @__PURE__ */ React3.createElement(Statistics, null, metrics.map((m, i) => /* @__PURE__ */ React3.createElement(
|
|
1848
|
+
StatisticsItem,
|
|
1849
|
+
{
|
|
1850
|
+
key: m.id || m.label || i,
|
|
1851
|
+
label: m.label,
|
|
1852
|
+
number: m.number != null ? String(m.number) : ""
|
|
1853
|
+
},
|
|
1854
|
+
m.trend ? /* @__PURE__ */ React3.createElement(
|
|
1855
|
+
StatisticsTrend,
|
|
1856
|
+
{
|
|
1857
|
+
direction: m.trend.direction || "increase",
|
|
1858
|
+
value: m.trend.value,
|
|
1859
|
+
color: m.trend.color
|
|
1860
|
+
}
|
|
1861
|
+
) : null
|
|
1862
|
+
)));
|
|
1863
|
+
};
|
|
1864
|
+
var KanbanToolbar = ({
|
|
1865
|
+
showSearch,
|
|
1866
|
+
searchValue,
|
|
1867
|
+
searchPlaceholder,
|
|
1868
|
+
onSearchChange,
|
|
1869
|
+
filters,
|
|
1870
|
+
filterValues,
|
|
1871
|
+
onFilterChange,
|
|
1872
|
+
filterInlineLimit,
|
|
1873
|
+
showFilterBadges,
|
|
1874
|
+
showClearFiltersButton,
|
|
1875
|
+
activeChips,
|
|
1876
|
+
onFilterRemove,
|
|
1877
|
+
sortOptions,
|
|
1878
|
+
sortValue,
|
|
1879
|
+
onSortChange,
|
|
1880
|
+
metrics,
|
|
1881
|
+
showMetrics,
|
|
1882
|
+
onToggleMetrics,
|
|
1883
|
+
labels
|
|
1884
|
+
}) => {
|
|
1885
|
+
const [showMoreFilters, setShowMoreFilters] = useState2(false);
|
|
1886
|
+
const inlineFilters = (filters || []).slice(0, filterInlineLimit);
|
|
1887
|
+
const overflowFilters = (filters || []).slice(filterInlineLimit);
|
|
1888
|
+
return /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", gap: "sm" }, /* @__PURE__ */ React3.createElement(Box2, { flex: 3 }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "sm" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch ? /* @__PURE__ */ React3.createElement(
|
|
1889
|
+
SearchInput2,
|
|
1890
|
+
{
|
|
1891
|
+
name: "kanban-search",
|
|
1892
|
+
placeholder: searchPlaceholder,
|
|
1893
|
+
value: searchValue,
|
|
1894
|
+
onChange: onSearchChange
|
|
1895
|
+
}
|
|
1896
|
+
) : null, inlineFilters.map(
|
|
1897
|
+
(filter) => renderFilterControl({
|
|
1898
|
+
filter,
|
|
1899
|
+
value: filterValues[filter.name],
|
|
1900
|
+
onChange: onFilterChange,
|
|
1901
|
+
labels
|
|
1902
|
+
})
|
|
1903
|
+
), overflowFilters.length > 0 ? /* @__PURE__ */ React3.createElement(
|
|
1904
|
+
Button2,
|
|
1905
|
+
{
|
|
1906
|
+
variant: "transparent",
|
|
1907
|
+
size: "small",
|
|
1908
|
+
onClick: () => setShowMoreFilters((prev) => !prev)
|
|
1909
|
+
},
|
|
1910
|
+
/* @__PURE__ */ React3.createElement(Icon2, { name: "filter", size: "sm" }),
|
|
1911
|
+
" ",
|
|
1912
|
+
labels.filtersButton
|
|
1913
|
+
) : null), showMoreFilters && overflowFilters.length > 0 ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, overflowFilters.map(
|
|
1914
|
+
(filter) => renderFilterControl({
|
|
1915
|
+
filter,
|
|
1916
|
+
value: filterValues[filter.name],
|
|
1917
|
+
onChange: onFilterChange,
|
|
1918
|
+
labels
|
|
1919
|
+
})
|
|
1920
|
+
)) : null, activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges ? activeChips.map((chip) => /* @__PURE__ */ React3.createElement(
|
|
1921
|
+
Tag3,
|
|
1922
|
+
{
|
|
1923
|
+
key: chip.key,
|
|
1924
|
+
variant: "default",
|
|
1925
|
+
onDelete: () => onFilterRemove(chip.key)
|
|
1926
|
+
},
|
|
1927
|
+
chip.label
|
|
1928
|
+
)) : null, showClearFiltersButton ? /* @__PURE__ */ React3.createElement(
|
|
1929
|
+
Button2,
|
|
1930
|
+
{
|
|
1931
|
+
variant: "transparent",
|
|
1932
|
+
size: "extra-small",
|
|
1933
|
+
onClick: () => onFilterRemove("all")
|
|
1934
|
+
},
|
|
1935
|
+
labels.clearAll
|
|
1936
|
+
) : null) : null)), (sortOptions == null ? void 0 : sortOptions.length) > 0 || metrics ? /* @__PURE__ */ React3.createElement(Box2, { flex: 1, alignSelf: "start" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", justify: "end" }, sortOptions && sortOptions.length > 0 ? /* @__PURE__ */ React3.createElement(
|
|
1937
|
+
Button2,
|
|
1938
|
+
{
|
|
1939
|
+
variant: "secondary",
|
|
1940
|
+
size: "small",
|
|
1941
|
+
overlay: /* @__PURE__ */ React3.createElement(Modal, { id: "kanban-sort-modal", title: labels.sortButton }, /* @__PURE__ */ React3.createElement(ModalBody, null, /* @__PURE__ */ React3.createElement(
|
|
1942
|
+
SortModalBody,
|
|
1943
|
+
{
|
|
1944
|
+
sortOptions,
|
|
1945
|
+
sortValue,
|
|
1946
|
+
onSortChange,
|
|
1947
|
+
labels
|
|
1948
|
+
}
|
|
1949
|
+
)))
|
|
1950
|
+
},
|
|
1951
|
+
/* @__PURE__ */ React3.createElement(Icon2, { name: "sortAmtDesc", size: "sm" }),
|
|
1952
|
+
" ",
|
|
1953
|
+
labels.sortButton
|
|
1954
|
+
) : null, metrics ? /* @__PURE__ */ React3.createElement(Button2, { variant: "secondary", size: "small", onClick: onToggleMetrics }, /* @__PURE__ */ React3.createElement(Icon2, { name: "reports", size: "sm" }), " ", labels.metricsButton) : null)) : null), showMetrics && metrics ? renderMetricsPanel(metrics) : null);
|
|
1955
|
+
};
|
|
1956
|
+
var DefaultSelectionBar = ({
|
|
1957
|
+
selectedIds,
|
|
1958
|
+
selectedCount,
|
|
1959
|
+
displayCount,
|
|
1960
|
+
countLabel,
|
|
1961
|
+
allSelected,
|
|
1962
|
+
onSelectAll,
|
|
1963
|
+
onDeselectAll,
|
|
1964
|
+
selectionActions,
|
|
1965
|
+
labels
|
|
1966
|
+
}) => {
|
|
1967
|
+
const pluralForCount = (n) => countLabel(n);
|
|
1968
|
+
return /* @__PURE__ */ React3.createElement(Tile2, { compact: true }, /* @__PURE__ */ React3.createElement(Inline, { align: "center", justify: "between", gap: "small" }, /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "small" }, /* @__PURE__ */ React3.createElement(Text2, { inline: true, format: { fontWeight: "demibold" } }, typeof labels.selected === "function" ? labels.selected(selectedCount, pluralForCount(selectedCount)) : `${selectedCount} selected`), !allSelected ? /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "extra-small", onClick: onSelectAll }, typeof labels.selectAll === "function" ? labels.selectAll(displayCount, pluralForCount(displayCount)) : labels.selectAll) : null, /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "extra-small", onClick: onDeselectAll }, labels.deselectAll)), (selectionActions || []).length > 0 ? /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "extra-small" }, selectionActions.map((action, i) => /* @__PURE__ */ React3.createElement(
|
|
1969
|
+
Button2,
|
|
1970
|
+
{
|
|
1971
|
+
key: action.key || action.label || i,
|
|
1972
|
+
variant: action.variant || "transparent",
|
|
1973
|
+
size: "extra-small",
|
|
1974
|
+
onClick: () => action.onClick([...selectedIds])
|
|
1975
|
+
},
|
|
1976
|
+
action.icon ? /* @__PURE__ */ React3.createElement(Icon2, { name: action.icon, size: "sm" }) : null,
|
|
1977
|
+
" ",
|
|
1978
|
+
action.label
|
|
1979
|
+
))) : null));
|
|
1980
|
+
};
|
|
1981
|
+
var Kanban = ({
|
|
1982
|
+
// --- Data ---
|
|
1983
|
+
data = [],
|
|
1984
|
+
stages = [],
|
|
1985
|
+
groupBy = "status",
|
|
1986
|
+
rowIdField = "id",
|
|
1987
|
+
// --- Card rendering ---
|
|
1988
|
+
renderCard,
|
|
1989
|
+
cardFields,
|
|
1990
|
+
cardDensity = DEFAULT_DENSITY,
|
|
1991
|
+
cardDividers,
|
|
1992
|
+
cardBodyAs = "descriptionList",
|
|
1993
|
+
maxBodyLines,
|
|
1994
|
+
maxCardsPerColumn = DEFAULT_MAX_CARDS,
|
|
1995
|
+
maxCardsExpanded = DEFAULT_MAX_EXPANDED,
|
|
1996
|
+
expandedStages,
|
|
1997
|
+
onExpandedStagesChange,
|
|
1998
|
+
// --- Per-stage pagination ---
|
|
1999
|
+
stageMeta,
|
|
2000
|
+
onLoadMore,
|
|
2001
|
+
// --- Selection ---
|
|
2002
|
+
selectable = false,
|
|
2003
|
+
selectedIds,
|
|
2004
|
+
onSelectionChange,
|
|
2005
|
+
selectionActions,
|
|
2006
|
+
recordLabel,
|
|
2007
|
+
selectionResetKey,
|
|
2008
|
+
resetSelectionOnQueryChange = true,
|
|
2009
|
+
showSelectionBar = true,
|
|
2010
|
+
renderSelectionBar,
|
|
2011
|
+
// --- Stage transitions ---
|
|
2012
|
+
stageControl,
|
|
2013
|
+
stageControlPlacement,
|
|
2014
|
+
onStageChange,
|
|
2015
|
+
isStageChanging,
|
|
2016
|
+
canMove,
|
|
2017
|
+
// --- Toolbar ---
|
|
2018
|
+
showSearch = true,
|
|
2019
|
+
searchFields,
|
|
2020
|
+
searchPlaceholder,
|
|
2021
|
+
searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
|
|
2022
|
+
fuzzySearch = false,
|
|
2023
|
+
fuzzyOptions,
|
|
2024
|
+
filters,
|
|
2025
|
+
filterInlineLimit = DEFAULT_FILTER_INLINE_LIMIT,
|
|
2026
|
+
showFilterBadges = true,
|
|
2027
|
+
showClearFiltersButton,
|
|
2028
|
+
sortOptions,
|
|
2029
|
+
defaultSort,
|
|
2030
|
+
sort,
|
|
2031
|
+
onSortChange,
|
|
2032
|
+
// --- Column level ---
|
|
2033
|
+
columnFooter,
|
|
2034
|
+
columnWidth = DEFAULT_COLUMN_WIDTH,
|
|
2035
|
+
countDisplay = "tag",
|
|
2036
|
+
collapsedStages,
|
|
2037
|
+
onCollapsedStagesChange,
|
|
2038
|
+
// --- Metrics panel ---
|
|
2039
|
+
metrics,
|
|
2040
|
+
// Array of stat items or a ReactNode for full override
|
|
2041
|
+
showMetrics: controlledShowMetrics,
|
|
2042
|
+
onMetricsToggle,
|
|
2043
|
+
// --- State (controlled) ---
|
|
2044
|
+
searchValue,
|
|
2045
|
+
onSearchChange,
|
|
2046
|
+
filterValues,
|
|
2047
|
+
onFilterChange,
|
|
2048
|
+
onParamsChange,
|
|
2049
|
+
loading = false,
|
|
2050
|
+
error,
|
|
2051
|
+
// --- Labels ---
|
|
2052
|
+
labels: labelsProp,
|
|
2053
|
+
renderEmptyState,
|
|
2054
|
+
renderLoadingState,
|
|
2055
|
+
renderErrorState
|
|
2056
|
+
}) => {
|
|
2057
|
+
var _a;
|
|
2058
|
+
const labels = useMemo2(() => ({ ...DEFAULT_LABELS, ...labelsProp || {} }), [labelsProp]);
|
|
2059
|
+
const [internalSearch, setInternalSearch] = useState2(searchValue != null ? searchValue : "");
|
|
2060
|
+
const [internalFilters, setInternalFilters] = useState2(() => {
|
|
2061
|
+
const init = {};
|
|
2062
|
+
(filters || []).forEach((f) => {
|
|
2063
|
+
init[f.name] = getEmptyFilterValue(f);
|
|
2064
|
+
});
|
|
2065
|
+
return init;
|
|
2066
|
+
});
|
|
2067
|
+
const [internalSort, setInternalSort] = useState2(defaultSort || (((_a = sortOptions == null ? void 0 : sortOptions[0]) == null ? void 0 : _a.value) ?? ""));
|
|
2068
|
+
const [internalCollapsed, setInternalCollapsed] = useState2([]);
|
|
2069
|
+
const [internalExpanded, setInternalExpanded] = useState2([]);
|
|
2070
|
+
const [internalSelection, setInternalSelection] = useState2([]);
|
|
2071
|
+
const [internalShowMetrics, setInternalShowMetrics] = useState2(false);
|
|
2072
|
+
const [transitionPrompts, setTransitionPrompts] = useState2({});
|
|
2073
|
+
const resolvedShowMetrics = controlledShowMetrics != null ? controlledShowMetrics : internalShowMetrics;
|
|
2074
|
+
const toggleMetrics = useCallback3(() => {
|
|
2075
|
+
const next = !resolvedShowMetrics;
|
|
2076
|
+
if (onMetricsToggle) onMetricsToggle(next);
|
|
2077
|
+
if (controlledShowMetrics == null) setInternalShowMetrics(next);
|
|
2078
|
+
}, [resolvedShowMetrics, onMetricsToggle, controlledShowMetrics]);
|
|
2079
|
+
const effectiveColumnWidth = Math.max(MIN_COLUMN_WIDTH, columnWidth || DEFAULT_COLUMN_WIDTH);
|
|
2080
|
+
const resolvedSearch = searchValue != null ? searchValue : internalSearch;
|
|
2081
|
+
const searchInputValue = searchDebounce > 0 ? internalSearch : resolvedSearch;
|
|
2082
|
+
const resolvedFilters = filterValues != null ? filterValues : internalFilters;
|
|
2083
|
+
const resolvedSort = sort != null ? sort : internalSort;
|
|
2084
|
+
const resolvedCollapsed = collapsedStages != null ? collapsedStages : internalCollapsed;
|
|
2085
|
+
const resolvedExpanded = expandedStages != null ? expandedStages : internalExpanded;
|
|
2086
|
+
const resolvedSelection = selectedIds != null ? selectedIds : internalSelection;
|
|
2087
|
+
const searchEnabled = showSearch && Array.isArray(searchFields) && searchFields.length > 0;
|
|
2088
|
+
const stagesByValue = useMemo2(() => {
|
|
2089
|
+
const map = {};
|
|
2090
|
+
for (const stage of stages || []) {
|
|
2091
|
+
map[stage.value] = stage;
|
|
2092
|
+
}
|
|
2093
|
+
return map;
|
|
2094
|
+
}, [stages]);
|
|
2095
|
+
const fireParamsChange = useCallback3((overrides = {}) => {
|
|
2096
|
+
if (!onParamsChange) return;
|
|
2097
|
+
onParamsChange({
|
|
2098
|
+
search: overrides.search != null ? overrides.search : resolvedSearch,
|
|
2099
|
+
filters: overrides.filters != null ? overrides.filters : resolvedFilters,
|
|
2100
|
+
sort: overrides.sort != null ? overrides.sort : resolvedSort || null,
|
|
2101
|
+
collapsedStages: overrides.collapsedStages != null ? overrides.collapsedStages : resolvedCollapsed
|
|
2102
|
+
});
|
|
2103
|
+
}, [onParamsChange, resolvedCollapsed, resolvedFilters, resolvedSearch, resolvedSort]);
|
|
2104
|
+
const lastAppliedSearchRef = useRef3(searchValue != null ? searchValue : "");
|
|
2105
|
+
useEffect3(() => {
|
|
2106
|
+
if (searchValue == null) return;
|
|
2107
|
+
if (searchValue === lastAppliedSearchRef.current) return;
|
|
2108
|
+
lastAppliedSearchRef.current = searchValue;
|
|
2109
|
+
setInternalSearch(searchValue);
|
|
2110
|
+
}, [searchValue]);
|
|
2111
|
+
const dispatchSearch = useCallback3(
|
|
2112
|
+
(val) => {
|
|
2113
|
+
lastAppliedSearchRef.current = val;
|
|
2114
|
+
if (onSearchChange) onSearchChange(val);
|
|
2115
|
+
fireParamsChange({ search: val });
|
|
2116
|
+
},
|
|
2117
|
+
[fireParamsChange, onSearchChange]
|
|
2118
|
+
);
|
|
2119
|
+
const dispatchSearchDebounced = useDebouncedDispatch(internalSearch, searchDebounce, dispatchSearch);
|
|
2120
|
+
const handleSearch = useCallback3(
|
|
2121
|
+
(val) => {
|
|
2122
|
+
setInternalSearch(val);
|
|
2123
|
+
dispatchSearchDebounced(val);
|
|
2124
|
+
},
|
|
2125
|
+
[dispatchSearchDebounced]
|
|
2126
|
+
);
|
|
2127
|
+
const handleFilter = useCallback3(
|
|
2128
|
+
(name, val) => {
|
|
2129
|
+
const next = { ...resolvedFilters, [name]: val };
|
|
2130
|
+
if (filterValues == null) setInternalFilters(next);
|
|
2131
|
+
if (onFilterChange) onFilterChange(next);
|
|
2132
|
+
fireParamsChange({ filters: next });
|
|
2133
|
+
},
|
|
2134
|
+
[fireParamsChange, onFilterChange, filterValues, resolvedFilters]
|
|
2135
|
+
);
|
|
2136
|
+
const handleFilterRemove = useCallback3(
|
|
2137
|
+
(key) => {
|
|
2138
|
+
if (key === "all") {
|
|
2139
|
+
const cleared = {};
|
|
2140
|
+
(filters || []).forEach((f) => {
|
|
2141
|
+
cleared[f.name] = getEmptyFilterValue(f);
|
|
2142
|
+
});
|
|
2143
|
+
if (filterValues == null) setInternalFilters(cleared);
|
|
2144
|
+
if (onFilterChange) onFilterChange(cleared);
|
|
2145
|
+
fireParamsChange({ filters: cleared });
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const filter = (filters || []).find((f) => f.name === key);
|
|
2149
|
+
const emptyVal = filter ? getEmptyFilterValue(filter) : "";
|
|
2150
|
+
const next = { ...resolvedFilters, [key]: emptyVal };
|
|
2151
|
+
if (filterValues == null) setInternalFilters(next);
|
|
2152
|
+
if (onFilterChange) onFilterChange(next);
|
|
2153
|
+
fireParamsChange({ filters: next });
|
|
2154
|
+
},
|
|
2155
|
+
[filters, filterValues, fireParamsChange, onFilterChange, resolvedFilters]
|
|
2156
|
+
);
|
|
2157
|
+
const handleSort = useCallback3(
|
|
2158
|
+
(val) => {
|
|
2159
|
+
if (onSortChange) onSortChange(val);
|
|
2160
|
+
if (sort == null) setInternalSort(val);
|
|
2161
|
+
fireParamsChange({ sort: val });
|
|
2162
|
+
},
|
|
2163
|
+
[fireParamsChange, onSortChange, sort]
|
|
2164
|
+
);
|
|
2165
|
+
const handleCollapsed = useCallback3(
|
|
2166
|
+
(stageValue) => {
|
|
2167
|
+
const next = resolvedCollapsed.includes(stageValue) ? resolvedCollapsed.filter((v) => v !== stageValue) : [...resolvedCollapsed, stageValue];
|
|
2168
|
+
if (onCollapsedStagesChange) onCollapsedStagesChange(next);
|
|
2169
|
+
if (collapsedStages == null) setInternalCollapsed(next);
|
|
2170
|
+
fireParamsChange({ collapsedStages: next });
|
|
2171
|
+
},
|
|
2172
|
+
[fireParamsChange, resolvedCollapsed, collapsedStages, onCollapsedStagesChange]
|
|
2173
|
+
);
|
|
2174
|
+
const handleExpanded = useCallback3(
|
|
2175
|
+
(stageValue) => {
|
|
2176
|
+
const next = resolvedExpanded.includes(stageValue) ? resolvedExpanded.filter((v) => v !== stageValue) : [...resolvedExpanded, stageValue];
|
|
2177
|
+
if (onExpandedStagesChange) onExpandedStagesChange(next);
|
|
2178
|
+
if (expandedStages == null) setInternalExpanded(next);
|
|
2179
|
+
},
|
|
2180
|
+
[resolvedExpanded, expandedStages, onExpandedStagesChange]
|
|
2181
|
+
);
|
|
2182
|
+
const handleToggleSelect = useCallback3(
|
|
2183
|
+
(rowId) => {
|
|
2184
|
+
const next = resolvedSelection.includes(rowId) ? resolvedSelection.filter((id) => id !== rowId) : [...resolvedSelection, rowId];
|
|
2185
|
+
if (onSelectionChange) onSelectionChange(next);
|
|
2186
|
+
if (selectedIds == null) setInternalSelection(next);
|
|
2187
|
+
},
|
|
2188
|
+
[resolvedSelection, selectedIds, onSelectionChange]
|
|
2189
|
+
);
|
|
2190
|
+
const clearTransitionPrompt = useCallback3((rowId) => {
|
|
2191
|
+
setTransitionPrompts((prev) => {
|
|
2192
|
+
if (!Object.prototype.hasOwnProperty.call(prev, rowId)) return prev;
|
|
2193
|
+
const next = { ...prev };
|
|
2194
|
+
delete next[rowId];
|
|
2195
|
+
return next;
|
|
2196
|
+
});
|
|
2197
|
+
}, []);
|
|
2198
|
+
const commitStageChange = useCallback3(
|
|
2199
|
+
(row, newStage, oldStage, result) => {
|
|
2200
|
+
clearTransitionPrompt(row[rowIdField]);
|
|
2201
|
+
if (onStageChange) onStageChange(row, newStage, oldStage, result);
|
|
2202
|
+
},
|
|
2203
|
+
[clearTransitionPrompt, onStageChange, rowIdField]
|
|
2204
|
+
);
|
|
2205
|
+
const selectionQueryKey = useMemo2(() => {
|
|
2206
|
+
if (!resetSelectionOnQueryChange) return "";
|
|
2207
|
+
return toStableKey({
|
|
2208
|
+
search: resolvedSearch,
|
|
2209
|
+
filters: resolvedFilters,
|
|
2210
|
+
sort: resolvedSort || null
|
|
2211
|
+
});
|
|
2212
|
+
}, [resetSelectionOnQueryChange, resolvedFilters, resolvedSearch, resolvedSort]);
|
|
2213
|
+
const combinedSelectionResetKey = useMemo2(
|
|
2214
|
+
() => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
|
|
2215
|
+
[selectionQueryKey, selectionResetKey]
|
|
2216
|
+
);
|
|
2217
|
+
const clearSelection = useCallback3(() => setInternalSelection([]), []);
|
|
2218
|
+
useSelectionReset({
|
|
2219
|
+
resetKey: combinedSelectionResetKey,
|
|
2220
|
+
enabled: selectable,
|
|
2221
|
+
isControlled: selectedIds != null,
|
|
2222
|
+
clearSelection
|
|
2223
|
+
});
|
|
2224
|
+
const getStageFor = useCallback3(
|
|
2225
|
+
(row) => {
|
|
2226
|
+
if (typeof groupBy === "function") return groupBy(row);
|
|
2227
|
+
return row[groupBy];
|
|
2228
|
+
},
|
|
2229
|
+
[groupBy]
|
|
2230
|
+
);
|
|
2231
|
+
const filteredData = useMemo2(() => {
|
|
2232
|
+
let result = filterRows(data, filters, resolvedFilters);
|
|
2233
|
+
const searchLower = (resolvedSearch || "").toLowerCase().trim();
|
|
2234
|
+
if (searchEnabled && searchLower) {
|
|
2235
|
+
result = searchRows(result, searchLower, searchFields, {
|
|
2236
|
+
fuzzy: fuzzySearch,
|
|
2237
|
+
fuzzyOptions
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
return result;
|
|
2241
|
+
}, [data, resolvedSearch, resolvedFilters, filters, searchEnabled, searchFields, fuzzySearch, fuzzyOptions]);
|
|
2242
|
+
const buckets = useMemo2(() => {
|
|
2243
|
+
const map = {};
|
|
2244
|
+
for (const stage of stages) map[stage.value] = [];
|
|
2245
|
+
for (const row of filteredData) {
|
|
2246
|
+
const key = getStageFor(row);
|
|
2247
|
+
if (map[key]) {
|
|
2248
|
+
map[key].push(row);
|
|
2249
|
+
} else if (stages.length > 0) {
|
|
2250
|
+
if (!map.__unknown) map.__unknown = [];
|
|
2251
|
+
map.__unknown.push(row);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return map;
|
|
2255
|
+
}, [filteredData, stages, getStageFor]);
|
|
2256
|
+
const sortComparator = useMemo2(() => {
|
|
2257
|
+
if (!sortOptions || !resolvedSort) return null;
|
|
2258
|
+
const opt = sortOptions.find((s) => s.value === resolvedSort);
|
|
2259
|
+
return (opt == null ? void 0 : opt.comparator) || null;
|
|
2260
|
+
}, [sortOptions, resolvedSort]);
|
|
2261
|
+
const sortedBuckets = useMemo2(() => {
|
|
2262
|
+
if (!sortComparator) return buckets;
|
|
2263
|
+
const out = {};
|
|
2264
|
+
for (const key of Object.keys(buckets)) {
|
|
2265
|
+
out[key] = [...buckets[key]].sort(sortComparator);
|
|
2266
|
+
}
|
|
2267
|
+
return out;
|
|
2268
|
+
}, [buckets, sortComparator]);
|
|
2269
|
+
const activeChips = useMemo2(() => {
|
|
2270
|
+
const chips = [];
|
|
2271
|
+
for (const filter of filters || []) {
|
|
2272
|
+
const val = resolvedFilters[filter.name];
|
|
2273
|
+
if (!isFilterActive(filter, val)) continue;
|
|
2274
|
+
const type = filter.type || "select";
|
|
2275
|
+
const prefix = filter.chipLabel || filter.placeholder || filter.name;
|
|
2276
|
+
if (type === "multiselect") {
|
|
2277
|
+
const labelList = val.map((v) => {
|
|
2278
|
+
var _a2;
|
|
2279
|
+
return ((_a2 = filter.options.find((o) => o.value === v)) == null ? void 0 : _a2.label) || v;
|
|
2280
|
+
}).join(", ");
|
|
2281
|
+
chips.push({ key: filter.name, label: `${prefix}: ${labelList}` });
|
|
2282
|
+
} else if (type === "dateRange") {
|
|
2283
|
+
const parts = [];
|
|
2284
|
+
if (val.from) parts.push(`from ${formatDateChip(val.from)}`);
|
|
2285
|
+
if (val.to) parts.push(`to ${formatDateChip(val.to)}`);
|
|
2286
|
+
chips.push({ key: filter.name, label: `${prefix}: ${parts.join(" ")}` });
|
|
2287
|
+
} else {
|
|
2288
|
+
const opt = filter.options.find((o) => o.value === val);
|
|
2289
|
+
chips.push({ key: filter.name, label: `${prefix}: ${(opt == null ? void 0 : opt.label) || val}` });
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return chips;
|
|
2293
|
+
}, [filters, resolvedFilters]);
|
|
2294
|
+
const partitioned = useMemo2(() => partitionFields(cardFields || []), [cardFields]);
|
|
2295
|
+
const dividers = useMemo2(() => resolveDividers(cardDividers, cardDensity), [cardDividers, cardDensity]);
|
|
2296
|
+
const resolvedMaxBody = maxBodyLines || (cardDensity === "comfortable" ? 5 : 3);
|
|
2297
|
+
const resolvedStageControl = stageControl || (cardDensity === "comfortable" ? "select" : "menu");
|
|
2298
|
+
const resolvedStageControlPlacement = stageControlPlacement || (resolvedStageControl === "menu" ? "inline" : "separateRow");
|
|
2299
|
+
const handleStageChangeRequest = useCallback3(
|
|
2300
|
+
(row, newStage, oldStage) => {
|
|
2301
|
+
var _a2;
|
|
2302
|
+
if (!newStage || newStage === oldStage) return;
|
|
2303
|
+
const targetStage = stagesByValue[newStage];
|
|
2304
|
+
if (!targetStage || !canStageReceiveRow(targetStage, row, canMove)) return;
|
|
2305
|
+
const rowId = row[rowIdField];
|
|
2306
|
+
if ((_a2 = targetStage.onEnterRequired) == null ? void 0 : _a2.render) {
|
|
2307
|
+
setTransitionPrompts((prev) => ({
|
|
2308
|
+
...prev,
|
|
2309
|
+
[rowId]: {
|
|
2310
|
+
row,
|
|
2311
|
+
fromStage: oldStage,
|
|
2312
|
+
toStage: newStage
|
|
2313
|
+
}
|
|
2314
|
+
}));
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
commitStageChange(row, newStage, oldStage);
|
|
2318
|
+
},
|
|
2319
|
+
[canMove, commitStageChange, rowIdField, stagesByValue]
|
|
2320
|
+
);
|
|
2321
|
+
const renderCardNode = useCallback3(
|
|
2322
|
+
(row, stage) => {
|
|
2323
|
+
var _a2;
|
|
2324
|
+
const rowId = row[rowIdField];
|
|
2325
|
+
const activePrompt = transitionPrompts[rowId];
|
|
2326
|
+
const promptStage = activePrompt ? stagesByValue[activePrompt.toStage] : null;
|
|
2327
|
+
if ((_a2 = promptStage == null ? void 0 : promptStage.onEnterRequired) == null ? void 0 : _a2.render) {
|
|
2328
|
+
return /* @__PURE__ */ React3.createElement(Tile2, { key: rowId, compact: cardDensity === "compact" }, promptStage.onEnterRequired.render({
|
|
2329
|
+
row: activePrompt.row,
|
|
2330
|
+
fromStage: activePrompt.fromStage,
|
|
2331
|
+
toStage: activePrompt.toStage,
|
|
2332
|
+
onConfirm: (result) => commitStageChange(activePrompt.row, activePrompt.toStage, activePrompt.fromStage, result),
|
|
2333
|
+
onCancel: () => clearTransitionPrompt(rowId)
|
|
2334
|
+
}));
|
|
2335
|
+
}
|
|
2336
|
+
if (renderCard) {
|
|
2337
|
+
return renderCard(row, {
|
|
2338
|
+
stage,
|
|
2339
|
+
isChanging: isStageChanging ? isStageChanging(row) : false,
|
|
2340
|
+
density: cardDensity,
|
|
2341
|
+
onStageChange: (newStage) => handleStageChangeRequest(row, newStage, stage.value)
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
return /* @__PURE__ */ React3.createElement(
|
|
2345
|
+
KanbanCard,
|
|
2346
|
+
{
|
|
2347
|
+
key: rowId,
|
|
2348
|
+
row,
|
|
2349
|
+
rowId,
|
|
2350
|
+
stage,
|
|
2351
|
+
stages,
|
|
2352
|
+
fields: partitioned,
|
|
2353
|
+
density: cardDensity,
|
|
2354
|
+
dividers,
|
|
2355
|
+
bodyAs: cardBodyAs,
|
|
2356
|
+
maxBodyLines: resolvedMaxBody,
|
|
2357
|
+
stageControl: resolvedStageControl,
|
|
2358
|
+
stageControlPlacement: resolvedStageControlPlacement,
|
|
2359
|
+
canMove,
|
|
2360
|
+
onStageChangeRequest: handleStageChangeRequest,
|
|
2361
|
+
isChanging: isStageChanging ? isStageChanging(row) : false,
|
|
2362
|
+
selectable,
|
|
2363
|
+
selected: resolvedSelection.includes(rowId),
|
|
2364
|
+
onToggleSelect: handleToggleSelect,
|
|
2365
|
+
labels
|
|
2366
|
+
}
|
|
2367
|
+
);
|
|
2368
|
+
},
|
|
2369
|
+
[
|
|
2370
|
+
clearTransitionPrompt,
|
|
2371
|
+
commitStageChange,
|
|
2372
|
+
renderCard,
|
|
2373
|
+
rowIdField,
|
|
2374
|
+
partitioned,
|
|
2375
|
+
cardDensity,
|
|
2376
|
+
dividers,
|
|
2377
|
+
resolvedMaxBody,
|
|
2378
|
+
resolvedStageControl,
|
|
2379
|
+
resolvedStageControlPlacement,
|
|
2380
|
+
canMove,
|
|
2381
|
+
isStageChanging,
|
|
2382
|
+
selectable,
|
|
2383
|
+
resolvedSelection,
|
|
2384
|
+
handleStageChangeRequest,
|
|
2385
|
+
handleToggleSelect,
|
|
2386
|
+
labels,
|
|
2387
|
+
stages,
|
|
2388
|
+
stagesByValue,
|
|
2389
|
+
transitionPrompts
|
|
2390
|
+
]
|
|
2391
|
+
);
|
|
2392
|
+
const totalMatching = filteredData.length;
|
|
2393
|
+
const selectedCount = resolvedSelection.length;
|
|
2394
|
+
const singular = ((recordLabel == null ? void 0 : recordLabel.singular) || "card").toLowerCase();
|
|
2395
|
+
const plural = ((recordLabel == null ? void 0 : recordLabel.plural) || "cards").toLowerCase();
|
|
2396
|
+
const countLabel = (n) => n === 1 ? singular : plural;
|
|
2397
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? ((recordLabel == null ? void 0 : recordLabel.plural) ? `Search ${plural}...` : labels.search);
|
|
2398
|
+
const selectionBarProps = {
|
|
2399
|
+
selectedIds: resolvedSelection,
|
|
2400
|
+
selectedCount,
|
|
2401
|
+
displayCount: totalMatching,
|
|
2402
|
+
countLabel,
|
|
2403
|
+
allSelected: selectedCount >= totalMatching && totalMatching > 0,
|
|
2404
|
+
onSelectAll: () => {
|
|
2405
|
+
const allIds = filteredData.map((r) => r[rowIdField]);
|
|
2406
|
+
if (onSelectionChange) onSelectionChange(allIds);
|
|
2407
|
+
if (selectedIds == null) setInternalSelection(allIds);
|
|
2408
|
+
},
|
|
2409
|
+
onDeselectAll: () => {
|
|
2410
|
+
if (onSelectionChange) onSelectionChange([]);
|
|
2411
|
+
if (selectedIds == null) setInternalSelection([]);
|
|
2412
|
+
},
|
|
2413
|
+
selectionActions: selectionActions || [],
|
|
2414
|
+
labels
|
|
2415
|
+
};
|
|
2416
|
+
const mainContent = error ? renderErrorState ? renderErrorState({
|
|
2417
|
+
error,
|
|
2418
|
+
title: labels.errorTitle,
|
|
2419
|
+
message: typeof error === "string" ? error : labels.errorMessage
|
|
2420
|
+
}) : /* @__PURE__ */ React3.createElement(Alert, { variant: "danger", title: labels.errorTitle }, typeof error === "string" ? error : labels.errorMessage) : loading && data.length === 0 ? renderLoadingState ? renderLoadingState({ label: labels.loading }) : (
|
|
2421
|
+
// Same EmptyState layout as the empty state (just the "building" image +
|
|
2422
|
+
// a loading message) so loading and empty match with no layout shift.
|
|
2423
|
+
/* @__PURE__ */ React3.createElement(Tile2, null, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React3.createElement(EmptyState2, { title: labels.loading, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React3.createElement(Text2, null, labels.loadingMessage))))
|
|
2424
|
+
) : filteredData.length === 0 ? renderEmptyState ? renderEmptyState({
|
|
2425
|
+
title: labels.emptyTitle,
|
|
2426
|
+
message: labels.emptyMessage
|
|
2427
|
+
}) : /* @__PURE__ */ React3.createElement(Tile2, null, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React3.createElement(EmptyState2, { title: labels.emptyTitle, layout: "vertical" }, /* @__PURE__ */ React3.createElement(Text2, null, labels.emptyMessage)))) : /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", gap: "sm", wrap: "nowrap" }, stages.map((stage) => {
|
|
2428
|
+
const stageRows = sortedBuckets[stage.value] || [];
|
|
2429
|
+
const meta = stageMeta == null ? void 0 : stageMeta[stage.value];
|
|
2430
|
+
const isExpanded = resolvedExpanded.includes(stage.value);
|
|
2431
|
+
const clamp = isExpanded ? maxCardsExpanded : maxCardsPerColumn;
|
|
2432
|
+
const visibleRows = stageRows.slice(0, clamp);
|
|
2433
|
+
const isCollapsed = resolvedCollapsed.includes(stage.value);
|
|
2434
|
+
return /* @__PURE__ */ React3.createElement(AutoGrid, { key: stage.value, columnWidth: isCollapsed ? 72 : effectiveColumnWidth }, /* @__PURE__ */ React3.createElement(
|
|
2435
|
+
KanbanColumn,
|
|
2436
|
+
{
|
|
2437
|
+
stage,
|
|
2438
|
+
rows: visibleRows,
|
|
2439
|
+
bucketCount: stageRows.length,
|
|
2440
|
+
totalCount: meta == null ? void 0 : meta.totalCount,
|
|
2441
|
+
hasMore: meta == null ? void 0 : meta.hasMore,
|
|
2442
|
+
loading: meta == null ? void 0 : meta.loading,
|
|
2443
|
+
error: meta == null ? void 0 : meta.error,
|
|
2444
|
+
onLoadMore,
|
|
2445
|
+
expanded: isExpanded,
|
|
2446
|
+
onToggleExpanded: () => handleExpanded(stage.value),
|
|
2447
|
+
collapsed: isCollapsed,
|
|
2448
|
+
onToggleCollapsed: () => handleCollapsed(stage.value),
|
|
2449
|
+
columnFooter,
|
|
2450
|
+
countDisplay,
|
|
2451
|
+
labels
|
|
2452
|
+
},
|
|
2453
|
+
visibleRows.map((row) => renderCardNode(row, stage))
|
|
2454
|
+
));
|
|
2455
|
+
}));
|
|
2456
|
+
const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
|
|
2457
|
+
return /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "sm" }, /* @__PURE__ */ React3.createElement(
|
|
2458
|
+
KanbanToolbar,
|
|
2459
|
+
{
|
|
2460
|
+
showSearch: searchEnabled,
|
|
2461
|
+
searchValue: searchInputValue,
|
|
2462
|
+
searchPlaceholder: resolvedSearchPlaceholder,
|
|
2463
|
+
onSearchChange: handleSearch,
|
|
2464
|
+
filters,
|
|
2465
|
+
filterValues: resolvedFilters,
|
|
2466
|
+
onFilterChange: handleFilter,
|
|
2467
|
+
filterInlineLimit,
|
|
2468
|
+
showFilterBadges,
|
|
2469
|
+
showClearFiltersButton: resolvedShowClearFiltersButton,
|
|
2470
|
+
activeChips,
|
|
2471
|
+
onFilterRemove: handleFilterRemove,
|
|
2472
|
+
sortOptions,
|
|
2473
|
+
sortValue: resolvedSort,
|
|
2474
|
+
onSortChange: handleSort,
|
|
2475
|
+
metrics,
|
|
2476
|
+
showMetrics: resolvedShowMetrics,
|
|
2477
|
+
onToggleMetrics: toggleMetrics,
|
|
2478
|
+
labels
|
|
2479
|
+
}
|
|
2480
|
+
), showSelectionBar && selectable && selectedCount > 0 ? renderSelectionBar ? renderSelectionBar(selectionBarProps) : /* @__PURE__ */ React3.createElement(DefaultSelectionBar, { ...selectionBarProps }) : null, mainContent);
|
|
2481
|
+
};
|
|
2482
|
+
|
|
2483
|
+
// src/utils/objectPath.js
|
|
2484
|
+
var getByPath = (obj, path) => {
|
|
2485
|
+
if (!path) return void 0;
|
|
2486
|
+
if (typeof path === "function") return path(obj);
|
|
2487
|
+
return String(path).split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
// src/utils/crmSearchAdapters.js
|
|
2491
|
+
var EMPTY_ARRAY = [];
|
|
2492
|
+
var EMPTY_OBJECT = {};
|
|
2493
|
+
var EMPTY_CRM_PARAMS = { search: "", filters: {}, sort: null };
|
|
2494
|
+
var isPlainObject = (value) => value != null && Object.prototype.toString.call(value) === "[object Object]";
|
|
2495
|
+
var coerceError = (error) => {
|
|
2496
|
+
if (!error) return false;
|
|
2497
|
+
if (typeof error === "string") return error;
|
|
2498
|
+
if (error.message) return error.message;
|
|
2499
|
+
return true;
|
|
2500
|
+
};
|
|
2501
|
+
var pickArray = (response) => {
|
|
2502
|
+
if (Array.isArray(response)) return response;
|
|
2503
|
+
if (!response) return EMPTY_ARRAY;
|
|
2504
|
+
return response.results || response.data || response.items || response.records || response.objects || EMPTY_ARRAY;
|
|
2505
|
+
};
|
|
2506
|
+
var pickTotal = (response, fallbackLength) => {
|
|
2507
|
+
var _a;
|
|
2508
|
+
if (!response || Array.isArray(response)) return fallbackLength;
|
|
2509
|
+
return response.total ?? response.totalCount ?? response.totalResults ?? ((_a = response.paging) == null ? void 0 : _a.total) ?? fallbackLength;
|
|
2510
|
+
};
|
|
2511
|
+
var normalizeCrmSearchRecord = (record, options = EMPTY_OBJECT) => {
|
|
2512
|
+
const {
|
|
2513
|
+
idField = "id",
|
|
2514
|
+
objectIdField = "objectId",
|
|
2515
|
+
propertiesKey = "properties",
|
|
2516
|
+
flattenProperties = true,
|
|
2517
|
+
propertyValueKey,
|
|
2518
|
+
mapRecord
|
|
2519
|
+
} = options;
|
|
2520
|
+
if (mapRecord) return mapRecord(record);
|
|
2521
|
+
const objectId = (record == null ? void 0 : record.objectId) ?? (record == null ? void 0 : record.id) ?? (record == null ? void 0 : record.hs_object_id) ?? getByPath(record, `${propertiesKey}.hs_object_id`);
|
|
2522
|
+
const properties = (record == null ? void 0 : record[propertiesKey]) || EMPTY_OBJECT;
|
|
2523
|
+
const flattened = {};
|
|
2524
|
+
if (flattenProperties && isPlainObject(properties)) {
|
|
2525
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
2526
|
+
flattened[key] = propertyValueKey && isPlainObject(value) ? value[propertyValueKey] : value;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return {
|
|
2530
|
+
...flattenProperties ? flattened : EMPTY_OBJECT,
|
|
2531
|
+
...record,
|
|
2532
|
+
[idField]: objectId,
|
|
2533
|
+
[objectIdField]: objectId,
|
|
2534
|
+
[propertiesKey]: properties
|
|
2535
|
+
};
|
|
2536
|
+
};
|
|
2537
|
+
var normalizeCrmSearchRows = (response, options = EMPTY_OBJECT) => {
|
|
2538
|
+
const records = pickArray(response);
|
|
2539
|
+
return records.map((record) => normalizeCrmSearchRecord(record, options));
|
|
2540
|
+
};
|
|
2541
|
+
var STABLE_SORT_TIEBREAKER = { propertyName: "hs_object_id", direction: "ASCENDING" };
|
|
2542
|
+
var withStableSort = (sorts) => {
|
|
2543
|
+
const base = Array.isArray(sorts) ? sorts : [];
|
|
2544
|
+
if (base.some((s) => s && s.propertyName === STABLE_SORT_TIEBREAKER.propertyName)) return base;
|
|
2545
|
+
return [...base, STABLE_SORT_TIEBREAKER];
|
|
2546
|
+
};
|
|
2547
|
+
var buildCrmSearchConfig = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
|
|
2548
|
+
const {
|
|
2549
|
+
objectType,
|
|
2550
|
+
properties = EMPTY_ARRAY,
|
|
2551
|
+
query,
|
|
2552
|
+
filterGroups,
|
|
2553
|
+
sorts,
|
|
2554
|
+
pageLength,
|
|
2555
|
+
propertyMap = EMPTY_OBJECT,
|
|
2556
|
+
filterMap,
|
|
2557
|
+
sortMap,
|
|
2558
|
+
baseConfig = EMPTY_OBJECT
|
|
2559
|
+
} = options;
|
|
2560
|
+
const mappedFilters = filterMap ? filterMap(params.filters || EMPTY_OBJECT, params) : filterGroups;
|
|
2561
|
+
const mappedSorts = sortMap ? sortMap(params.sort || EMPTY_OBJECT, params) : sorts;
|
|
2562
|
+
const config = {
|
|
2563
|
+
...baseConfig,
|
|
2564
|
+
objectType: objectType || baseConfig.objectType,
|
|
2565
|
+
properties: properties.length ? properties : baseConfig.properties,
|
|
2566
|
+
query: query ?? params.search ?? baseConfig.query,
|
|
2567
|
+
filterGroups: mappedFilters ?? baseConfig.filterGroups,
|
|
2568
|
+
sorts: withStableSort(mappedSorts ?? baseConfig.sorts),
|
|
2569
|
+
pageLength: pageLength ?? params.pageLength ?? baseConfig.pageLength
|
|
2570
|
+
};
|
|
2571
|
+
if (propertyMap && Object.keys(propertyMap).length && params.filters) {
|
|
2572
|
+
const filters = Object.entries(params.filters).filter(([, value]) => value !== void 0 && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0)).map(([name, value]) => {
|
|
2573
|
+
const propertyName = propertyMap[name] || name;
|
|
2574
|
+
if (Array.isArray(value)) return { propertyName, operator: "IN", values: value };
|
|
2575
|
+
if (isPlainObject(value) && (value.from || value.to)) {
|
|
2576
|
+
const rangeFilters = [];
|
|
2577
|
+
if (value.from) rangeFilters.push({ propertyName, operator: "GTE", value: value.from });
|
|
2578
|
+
if (value.to) rangeFilters.push({ propertyName, operator: "LTE", value: value.to });
|
|
2579
|
+
return rangeFilters;
|
|
2580
|
+
}
|
|
2581
|
+
return { propertyName, operator: "EQ", value };
|
|
2582
|
+
}).flat();
|
|
2583
|
+
if (filters.length) config.filterGroups = [{ filters }];
|
|
2584
|
+
}
|
|
2585
|
+
Object.keys(config).forEach((key) => config[key] === void 0 && delete config[key]);
|
|
2586
|
+
return config;
|
|
2587
|
+
};
|
|
2588
|
+
var useCrmSearchDataSource = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
|
|
2589
|
+
const {
|
|
2590
|
+
format,
|
|
2591
|
+
row,
|
|
2592
|
+
rowIdField = "id",
|
|
2593
|
+
totalCount,
|
|
2594
|
+
loading,
|
|
2595
|
+
error,
|
|
2596
|
+
mapResponse
|
|
2597
|
+
} = options;
|
|
2598
|
+
const config = useMemo3(() => buildCrmSearchConfig(params, options), [params, options]);
|
|
2599
|
+
const response = useCrmSearch(config, format);
|
|
2600
|
+
return useMemo3(() => {
|
|
2601
|
+
const rows = mapResponse ? mapResponse(response) : normalizeCrmSearchRows(response, { idField: rowIdField, ...row || EMPTY_OBJECT });
|
|
2602
|
+
const resolvedTotal = typeof totalCount === "function" ? totalCount(response) : totalCount ?? pickTotal(response, rows.length);
|
|
2603
|
+
return {
|
|
2604
|
+
data: rows,
|
|
2605
|
+
rows,
|
|
2606
|
+
response,
|
|
2607
|
+
loading: typeof loading === "function" ? loading(response) : loading ?? !!(response == null ? void 0 : response.isLoading),
|
|
2608
|
+
isLoading: typeof loading === "function" ? loading(response) : loading ?? !!(response == null ? void 0 : response.isLoading),
|
|
2609
|
+
error: typeof error === "function" ? error(response) : error ?? coerceError(response == null ? void 0 : response.error),
|
|
2610
|
+
totalCount: resolvedTotal,
|
|
2611
|
+
rowIdField
|
|
2612
|
+
};
|
|
2613
|
+
}, [response, mapResponse, row, rowIdField, totalCount, loading, error]);
|
|
2614
|
+
};
|
|
2615
|
+
var crmSearchResultToOption = (row, options = EMPTY_OBJECT) => {
|
|
2616
|
+
const {
|
|
2617
|
+
label = "name",
|
|
2618
|
+
value = "objectId",
|
|
2619
|
+
description,
|
|
2620
|
+
fallbackLabel = "Untitled record",
|
|
2621
|
+
mapOption
|
|
2622
|
+
} = options;
|
|
2623
|
+
if (mapOption) return mapOption(row);
|
|
2624
|
+
const option = {
|
|
2625
|
+
label: getByPath(row, label) ?? getByPath(row, "properties.name") ?? fallbackLabel,
|
|
2626
|
+
value: getByPath(row, value) ?? getByPath(row, "id") ?? getByPath(row, "objectId")
|
|
2627
|
+
};
|
|
2628
|
+
const desc = getByPath(row, description);
|
|
2629
|
+
if (desc != null && desc !== "") option.description = desc;
|
|
2630
|
+
return option;
|
|
2631
|
+
};
|
|
2632
|
+
var useCrmSearchOptions = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
|
|
2633
|
+
const dataSource = useCrmSearchDataSource(params, options);
|
|
2634
|
+
const optionConfig = options.option || options;
|
|
2635
|
+
return useMemo3(() => ({
|
|
2636
|
+
...dataSource,
|
|
2637
|
+
options: dataSource.rows.map((row) => crmSearchResultToOption(row, optionConfig))
|
|
2638
|
+
}), [dataSource, optionConfig]);
|
|
2639
|
+
};
|
|
2640
|
+
var makeCrmSearchSelectField = (field, searchOptions) => ({
|
|
2641
|
+
type: "select",
|
|
2642
|
+
...field,
|
|
2643
|
+
options: searchOptions.options || EMPTY_ARRAY,
|
|
2644
|
+
loading: searchOptions.loading || searchOptions.isLoading || (field == null ? void 0 : field.loading)
|
|
2645
|
+
});
|
|
2646
|
+
var makeCrmSearchMultiSelectField = (field, searchOptions) => ({
|
|
2647
|
+
type: "multiselect",
|
|
2648
|
+
...field,
|
|
2649
|
+
options: searchOptions.options || EMPTY_ARRAY,
|
|
2650
|
+
loading: searchOptions.loading || searchOptions.isLoading || (field == null ? void 0 : field.loading)
|
|
2651
|
+
});
|
|
2652
|
+
var CRM_OBJECT_TYPES = {
|
|
2653
|
+
contact: "0-1",
|
|
2654
|
+
contacts: "0-1",
|
|
2655
|
+
company: "0-2",
|
|
2656
|
+
companies: "0-2",
|
|
2657
|
+
deal: "0-3",
|
|
2658
|
+
deals: "0-3"
|
|
2659
|
+
};
|
|
2660
|
+
var prettifyPropertyName = (name) => String(name || "").replace(/^hs_/, "").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2661
|
+
var inferCrmColumns = (properties = EMPTY_ARRAY) => properties.map((property) => ({
|
|
2662
|
+
field: property,
|
|
2663
|
+
label: prettifyPropertyName(property),
|
|
2664
|
+
sortable: true
|
|
2665
|
+
}));
|
|
2666
|
+
var normalizeAutoFilterFields = (autoFilters, properties = EMPTY_ARRAY) => {
|
|
2667
|
+
if (!autoFilters) return EMPTY_ARRAY;
|
|
2668
|
+
if (Array.isArray(autoFilters)) return autoFilters;
|
|
2669
|
+
if (typeof autoFilters === "object" && Array.isArray(autoFilters.fields)) return autoFilters.fields;
|
|
2670
|
+
return properties.filter((property) => !["id", "objectId", "hs_object_id", "email", "firstname", "lastname", "name", "domain"].includes(property));
|
|
2671
|
+
};
|
|
2672
|
+
var buildAutoFiltersFromRows = ({ rows, fields, labelsRef, maxOptions = 25 }) => {
|
|
2673
|
+
if (!fields.length) return EMPTY_ARRAY;
|
|
2674
|
+
for (const row of rows || EMPTY_ARRAY) {
|
|
2675
|
+
for (const field of fields) {
|
|
2676
|
+
const value = getByPath(row, field);
|
|
2677
|
+
if (value == null || value === "" || Array.isArray(value) || isPlainObject(value)) continue;
|
|
2678
|
+
if (!labelsRef.current[field]) labelsRef.current[field] = /* @__PURE__ */ new Map();
|
|
2679
|
+
const map = labelsRef.current[field];
|
|
2680
|
+
if (map.size < maxOptions || map.has(value)) map.set(value, String(value));
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return fields.map((field) => {
|
|
2684
|
+
const map = labelsRef.current[field];
|
|
2685
|
+
if (!map || map.size === 0 || map.size > maxOptions) return null;
|
|
2686
|
+
return {
|
|
2687
|
+
name: field,
|
|
2688
|
+
label: prettifyPropertyName(field),
|
|
2689
|
+
placeholder: `Any ${prettifyPropertyName(field).toLowerCase()}`,
|
|
2690
|
+
options: Array.from(map.entries()).map(([value, label]) => ({ value, label }))
|
|
2691
|
+
};
|
|
2692
|
+
}).filter(Boolean);
|
|
2693
|
+
};
|
|
2694
|
+
var resolveCrmObjectType = (objectType) => CRM_OBJECT_TYPES[objectType] || objectType;
|
|
2695
|
+
var DEFAULT_CRM_FORMAT = { propertiesToFormat: "all" };
|
|
2696
|
+
var defaultCrmMapRecord = (record) => ({ objectId: record.objectId, ...record.properties });
|
|
2697
|
+
var crmSortsFromState = (sort, propertyMap) => {
|
|
2698
|
+
if (!sort || !sort.field || !sort.direction) return void 0;
|
|
2699
|
+
const propertyName = propertyMap && propertyMap[sort.field] || sort.field;
|
|
2700
|
+
return [{ propertyName, direction: sort.direction === "descending" ? "DESCENDING" : "ASCENDING" }];
|
|
2701
|
+
};
|
|
2702
|
+
var CrmDataTable = ({
|
|
2703
|
+
objectType,
|
|
2704
|
+
properties = EMPTY_ARRAY,
|
|
2705
|
+
columns,
|
|
2706
|
+
title,
|
|
2707
|
+
pageLength = 100,
|
|
2708
|
+
// CRM batch fetched per request (CRM search max)
|
|
2709
|
+
pageSize = 10,
|
|
2710
|
+
// client-side page size
|
|
2711
|
+
// Hybrid model: fetch ONE batch and do everything client-side while the whole
|
|
2712
|
+
// result set fits in the batch (no refetch). Once a fetch comes back capped
|
|
2713
|
+
// (more matches than the batch), search / filter / sort start refetching a
|
|
2714
|
+
// fresh batch server-side so they reach the whole dataset — pagination always
|
|
2715
|
+
// stays client-side (the broken useCrmSearch cursor is never used). Set
|
|
2716
|
+
// `serverSide` to force server-side querying from the first render.
|
|
2717
|
+
serverSide = false,
|
|
2718
|
+
filters,
|
|
2719
|
+
autoFilters = false,
|
|
2720
|
+
autoFilterMaxOptions = 25,
|
|
2721
|
+
filterMap,
|
|
2722
|
+
propertyMap,
|
|
2723
|
+
sortMap,
|
|
2724
|
+
searchFields,
|
|
2725
|
+
searchPlaceholder,
|
|
2726
|
+
format = DEFAULT_CRM_FORMAT,
|
|
2727
|
+
mapRecord,
|
|
2728
|
+
rowIdField = "objectId",
|
|
2729
|
+
dataTableProps = EMPTY_OBJECT,
|
|
2730
|
+
...props
|
|
2731
|
+
}) => {
|
|
2732
|
+
var _a;
|
|
2733
|
+
const [params, setParams] = useState3({ search: "", filters: {}, sort: null });
|
|
2734
|
+
const resolvedProperties = useMemo3(() => properties, [properties]);
|
|
2735
|
+
const resolvedColumns = useMemo3(
|
|
2736
|
+
() => columns || inferCrmColumns(resolvedProperties),
|
|
2737
|
+
[columns, resolvedProperties]
|
|
2738
|
+
);
|
|
2739
|
+
const resolvedSearchFields = searchFields || resolvedProperties;
|
|
2740
|
+
const autoFilterFields = useMemo3(
|
|
2741
|
+
() => normalizeAutoFilterFields(autoFilters, resolvedProperties),
|
|
2742
|
+
[autoFilters, resolvedProperties]
|
|
2743
|
+
);
|
|
2744
|
+
const autoFilterLabelsRef = useRef4({});
|
|
2745
|
+
const defaultPropertyMap = useMemo3(
|
|
2746
|
+
() => Object.fromEntries(resolvedProperties.map((property) => [property, property])),
|
|
2747
|
+
[resolvedProperties]
|
|
2748
|
+
);
|
|
2749
|
+
const effectivePropertyMap = propertyMap || defaultPropertyMap;
|
|
2750
|
+
const resolvedSortMap = useMemo3(
|
|
2751
|
+
() => sortMap || ((sort) => crmSortsFromState(sort, effectivePropertyMap)),
|
|
2752
|
+
[sortMap, effectivePropertyMap]
|
|
2753
|
+
);
|
|
2754
|
+
const resolvedMapRecord = mapRecord || defaultCrmMapRecord;
|
|
2755
|
+
const dataSourceOptions = useMemo3(
|
|
2756
|
+
() => ({
|
|
2757
|
+
objectType: resolveCrmObjectType(objectType),
|
|
2758
|
+
properties: resolvedProperties,
|
|
2759
|
+
pageLength,
|
|
2760
|
+
format,
|
|
2761
|
+
filterMap,
|
|
2762
|
+
propertyMap: effectivePropertyMap,
|
|
2763
|
+
sortMap: resolvedSortMap,
|
|
2764
|
+
rowIdField,
|
|
2765
|
+
row: { idField: rowIdField, mapRecord: resolvedMapRecord }
|
|
2766
|
+
}),
|
|
2767
|
+
[objectType, resolvedProperties, pageLength, format, filterMap, effectivePropertyMap, resolvedSortMap, rowIdField, resolvedMapRecord]
|
|
2768
|
+
);
|
|
2769
|
+
const [serverQuerying, setServerQuerying] = useState3(!!serverSide);
|
|
2770
|
+
const effectiveParams = serverQuerying ? params : EMPTY_CRM_PARAMS;
|
|
2771
|
+
const dataSource = useCrmSearchDataSource(effectiveParams, dataSourceOptions);
|
|
2772
|
+
useEffect4(() => {
|
|
2773
|
+
if (!serverQuerying && typeof dataSource.totalCount === "number" && dataSource.totalCount > dataSource.data.length) {
|
|
2774
|
+
setServerQuerying(true);
|
|
2775
|
+
}
|
|
2776
|
+
}, [serverQuerying, dataSource.totalCount, dataSource.data.length]);
|
|
2777
|
+
const generatedFilters = useMemo3(
|
|
2778
|
+
() => buildAutoFiltersFromRows({
|
|
2779
|
+
rows: dataSource.data,
|
|
2780
|
+
fields: autoFilterFields,
|
|
2781
|
+
labelsRef: autoFilterLabelsRef,
|
|
2782
|
+
maxOptions: autoFilterMaxOptions
|
|
2783
|
+
}),
|
|
2784
|
+
[dataSource.data, autoFilterFields, autoFilterMaxOptions]
|
|
2785
|
+
);
|
|
2786
|
+
const resolvedFilters = filters || generatedFilters;
|
|
2787
|
+
const table = React4.createElement(DataTable, {
|
|
2788
|
+
title: title || `${prettifyPropertyName(objectType)} records`,
|
|
2789
|
+
data: dataSource.data,
|
|
2790
|
+
loading: dataSource.loading || ((_a = dataSource.response) == null ? void 0 : _a.isRefetching),
|
|
2791
|
+
error: dataSource.error,
|
|
2792
|
+
columns: resolvedColumns,
|
|
2793
|
+
rowIdField,
|
|
2794
|
+
pageSize,
|
|
2795
|
+
filters: resolvedFilters,
|
|
2796
|
+
searchFields: resolvedSearchFields,
|
|
2797
|
+
searchPlaceholder: searchPlaceholder || `Search ${prettifyPropertyName(objectType).toLowerCase()}...`,
|
|
2798
|
+
searchDebounce: 300,
|
|
2799
|
+
onParamsChange: (next) => {
|
|
2800
|
+
setParams((prev) => ({ ...prev, search: next.search, filters: next.filters, sort: next.sort }));
|
|
2801
|
+
},
|
|
2802
|
+
...dataTableProps,
|
|
2803
|
+
...props
|
|
2804
|
+
});
|
|
2805
|
+
const total = dataSource.totalCount;
|
|
2806
|
+
const capped = typeof total === "number" && total > dataSource.data.length;
|
|
2807
|
+
if (!capped) return table;
|
|
2808
|
+
return React4.createElement(
|
|
2809
|
+
Flex3,
|
|
2810
|
+
{ direction: "column", gap: "xs" },
|
|
2811
|
+
React4.createElement(
|
|
2812
|
+
Text3,
|
|
2813
|
+
{ variant: "microcopy" },
|
|
2814
|
+
`Showing the first ${dataSource.data.length} of ${total} matching. Refine your search or filters to narrow the results.`
|
|
2815
|
+
),
|
|
2816
|
+
table
|
|
2817
|
+
);
|
|
2818
|
+
};
|
|
2819
|
+
var CrmKanban = ({
|
|
2820
|
+
objectType,
|
|
2821
|
+
properties = EMPTY_ARRAY,
|
|
2822
|
+
groupBy,
|
|
2823
|
+
stages,
|
|
2824
|
+
stageLabels,
|
|
2825
|
+
// object { value: label } or (value) => label
|
|
2826
|
+
title,
|
|
2827
|
+
pageLength = 100,
|
|
2828
|
+
serverSide = false,
|
|
2829
|
+
filters,
|
|
2830
|
+
autoFilters = false,
|
|
2831
|
+
autoFilterMaxOptions = 25,
|
|
2832
|
+
filterMap,
|
|
2833
|
+
propertyMap,
|
|
2834
|
+
sortMap,
|
|
2835
|
+
searchFields,
|
|
2836
|
+
searchPlaceholder,
|
|
2837
|
+
format = DEFAULT_CRM_FORMAT,
|
|
2838
|
+
mapRecord,
|
|
2839
|
+
rowIdField = "objectId",
|
|
2840
|
+
kanbanProps = EMPTY_OBJECT,
|
|
2841
|
+
...props
|
|
2842
|
+
}) => {
|
|
2843
|
+
var _a;
|
|
2844
|
+
const [params, setParams] = useState3(EMPTY_CRM_PARAMS);
|
|
2845
|
+
const resolvedProperties = useMemo3(() => properties, [properties]);
|
|
2846
|
+
const resolvedSearchFields = searchFields || resolvedProperties;
|
|
2847
|
+
const autoFilterFields = useMemo3(
|
|
2848
|
+
() => normalizeAutoFilterFields(autoFilters, resolvedProperties),
|
|
2849
|
+
[autoFilters, resolvedProperties]
|
|
2850
|
+
);
|
|
2851
|
+
const autoFilterLabelsRef = useRef4({});
|
|
2852
|
+
const defaultPropertyMap = useMemo3(
|
|
2853
|
+
() => Object.fromEntries(resolvedProperties.map((property) => [property, property])),
|
|
2854
|
+
[resolvedProperties]
|
|
2855
|
+
);
|
|
2856
|
+
const effectivePropertyMap = propertyMap || defaultPropertyMap;
|
|
2857
|
+
const resolvedSortMap = useMemo3(
|
|
2858
|
+
() => sortMap || ((sort) => crmSortsFromState(sort, effectivePropertyMap)),
|
|
2859
|
+
[sortMap, effectivePropertyMap]
|
|
2860
|
+
);
|
|
2861
|
+
const resolvedMapRecord = mapRecord || defaultCrmMapRecord;
|
|
2862
|
+
const dataSourceOptions = useMemo3(
|
|
2863
|
+
() => ({
|
|
2864
|
+
objectType: resolveCrmObjectType(objectType),
|
|
2865
|
+
properties: resolvedProperties,
|
|
2866
|
+
pageLength,
|
|
2867
|
+
format,
|
|
2868
|
+
filterMap,
|
|
2869
|
+
propertyMap: effectivePropertyMap,
|
|
2870
|
+
sortMap: resolvedSortMap,
|
|
2871
|
+
rowIdField,
|
|
2872
|
+
row: { idField: rowIdField, mapRecord: resolvedMapRecord }
|
|
2873
|
+
}),
|
|
2874
|
+
[objectType, resolvedProperties, pageLength, format, filterMap, effectivePropertyMap, resolvedSortMap, rowIdField, resolvedMapRecord]
|
|
2875
|
+
);
|
|
2876
|
+
const [serverQuerying, setServerQuerying] = useState3(!!serverSide);
|
|
2877
|
+
const effectiveParams = serverQuerying ? params : EMPTY_CRM_PARAMS;
|
|
2878
|
+
const dataSource = useCrmSearchDataSource(effectiveParams, dataSourceOptions);
|
|
2879
|
+
useEffect4(() => {
|
|
2880
|
+
if (!serverQuerying && typeof dataSource.totalCount === "number" && dataSource.totalCount > dataSource.data.length) {
|
|
2881
|
+
setServerQuerying(true);
|
|
2882
|
+
}
|
|
2883
|
+
}, [serverQuerying, dataSource.totalCount, dataSource.data.length]);
|
|
2884
|
+
const generatedFilters = useMemo3(
|
|
2885
|
+
() => buildAutoFiltersFromRows({
|
|
2886
|
+
rows: dataSource.data,
|
|
2887
|
+
fields: autoFilterFields,
|
|
2888
|
+
labelsRef: autoFilterLabelsRef,
|
|
2889
|
+
maxOptions: autoFilterMaxOptions
|
|
2890
|
+
}),
|
|
2891
|
+
[dataSource.data, autoFilterFields, autoFilterMaxOptions]
|
|
2892
|
+
);
|
|
2893
|
+
const resolvedFilters = filters || generatedFilters;
|
|
2894
|
+
const resolvedStages = useMemo3(() => {
|
|
2895
|
+
if (stages) return stages;
|
|
2896
|
+
const seen = [];
|
|
2897
|
+
for (const row of dataSource.data) {
|
|
2898
|
+
const value = typeof groupBy === "function" ? groupBy(row) : row[groupBy];
|
|
2899
|
+
if (value != null && value !== "" && !seen.includes(value)) seen.push(value);
|
|
2900
|
+
}
|
|
2901
|
+
return seen.map((value) => ({
|
|
2902
|
+
value,
|
|
2903
|
+
label: typeof stageLabels === "function" ? stageLabels(value) : stageLabels && stageLabels[value] || prettifyPropertyName(String(value))
|
|
2904
|
+
}));
|
|
2905
|
+
}, [stages, stageLabels, dataSource.data, groupBy]);
|
|
2906
|
+
const board = React4.createElement(Kanban, {
|
|
2907
|
+
title: title || `${prettifyPropertyName(objectType)} board`,
|
|
2908
|
+
data: dataSource.data,
|
|
2909
|
+
loading: dataSource.loading || ((_a = dataSource.response) == null ? void 0 : _a.isRefetching),
|
|
2910
|
+
error: dataSource.error,
|
|
2911
|
+
rowIdField,
|
|
2912
|
+
groupBy,
|
|
2913
|
+
stages: resolvedStages,
|
|
2914
|
+
filters: resolvedFilters,
|
|
2915
|
+
searchFields: resolvedSearchFields,
|
|
2916
|
+
searchPlaceholder: searchPlaceholder || `Search ${prettifyPropertyName(objectType).toLowerCase()}...`,
|
|
2917
|
+
searchDebounce: 300,
|
|
2918
|
+
onParamsChange: (next) => {
|
|
2919
|
+
setParams((prev) => ({ ...prev, search: next.search, filters: next.filters }));
|
|
2920
|
+
},
|
|
2921
|
+
...kanbanProps,
|
|
2922
|
+
...props
|
|
2923
|
+
});
|
|
2924
|
+
const total = dataSource.totalCount;
|
|
2925
|
+
const capped = typeof total === "number" && total > dataSource.data.length;
|
|
2926
|
+
if (!capped) return board;
|
|
2927
|
+
return React4.createElement(
|
|
2928
|
+
Flex3,
|
|
2929
|
+
{ direction: "column", gap: "xs" },
|
|
2930
|
+
React4.createElement(
|
|
2931
|
+
Text3,
|
|
2932
|
+
{ variant: "microcopy" },
|
|
2933
|
+
`Showing the first ${dataSource.data.length} of ${total} matching. Refine your search or filters to narrow the results.`
|
|
2934
|
+
),
|
|
2935
|
+
board
|
|
2936
|
+
);
|
|
2937
|
+
};
|
|
2938
|
+
|
|
7
2939
|
// src/utils/formatters.js
|
|
8
2940
|
var DEFAULT_LOCALE = "en-US";
|
|
9
2941
|
var formatCurrency = (value, { locale = DEFAULT_LOCALE, currency = "USD", maximumFractionDigits = 0, ...options } = {}) => new Intl.NumberFormat(locale, {
|
|
@@ -311,8 +3243,12 @@ var deriveCardFieldsFromColumns = (columns, opts = {}) => {
|
|
|
311
3243
|
return out;
|
|
312
3244
|
};
|
|
313
3245
|
export {
|
|
3246
|
+
CrmDataTable,
|
|
3247
|
+
CrmKanban,
|
|
3248
|
+
buildCrmSearchConfig,
|
|
314
3249
|
buildOptions,
|
|
315
3250
|
createStatusTagSortComparator,
|
|
3251
|
+
crmSearchResultToOption,
|
|
316
3252
|
deriveCardFieldsFromColumns,
|
|
317
3253
|
findOptionLabel,
|
|
318
3254
|
formatCurrency,
|
|
@@ -326,5 +3262,12 @@ export {
|
|
|
326
3262
|
isDateTimeValueObject,
|
|
327
3263
|
isDateValueObject,
|
|
328
3264
|
isTimeValueObject,
|
|
329
|
-
|
|
3265
|
+
makeCrmSearchMultiSelectField,
|
|
3266
|
+
makeCrmSearchSelectField,
|
|
3267
|
+
normalizeCrmSearchRecord,
|
|
3268
|
+
normalizeCrmSearchRows,
|
|
3269
|
+
resolveCrmObjectType,
|
|
3270
|
+
sumBy,
|
|
3271
|
+
useCrmSearchDataSource,
|
|
3272
|
+
useCrmSearchOptions
|
|
330
3273
|
};
|