hs-uix 1.6.5 → 2.0.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 +291 -239
- package/dist/datatable.mjs +207 -155
- package/dist/feed.js +939 -0
- package/dist/feed.mjs +927 -0
- package/dist/form.js +115 -93
- package/dist/form.mjs +115 -93
- package/dist/index.js +3529 -1060
- package/dist/index.mjs +3231 -770
- 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/feed.mjs
ADDED
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
// packages/feed/src/Feed.jsx
|
|
2
|
+
import React2, { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Alert,
|
|
5
|
+
Box,
|
|
6
|
+
Button,
|
|
7
|
+
ButtonRow,
|
|
8
|
+
DateInput,
|
|
9
|
+
DescriptionList,
|
|
10
|
+
DescriptionListItem,
|
|
11
|
+
Divider,
|
|
12
|
+
EmptyState,
|
|
13
|
+
Flex,
|
|
14
|
+
Icon,
|
|
15
|
+
Inline,
|
|
16
|
+
Link,
|
|
17
|
+
List,
|
|
18
|
+
MultiSelect,
|
|
19
|
+
SearchInput,
|
|
20
|
+
Select,
|
|
21
|
+
StatusTag,
|
|
22
|
+
Tab,
|
|
23
|
+
Tabs,
|
|
24
|
+
Tag,
|
|
25
|
+
Text,
|
|
26
|
+
Tile
|
|
27
|
+
} from "@hubspot/ui-extensions";
|
|
28
|
+
|
|
29
|
+
// src/common-components/AvatarStack.js
|
|
30
|
+
import React from "react";
|
|
31
|
+
import { Image } from "@hubspot/ui-extensions";
|
|
32
|
+
|
|
33
|
+
// src/common-components/svgDefaults.js
|
|
34
|
+
var HS_FONT_FAMILY = '"Lexend Deca", Helvetica, Arial, sans-serif';
|
|
35
|
+
var HS_TEXT_COLOR = "#33475b";
|
|
36
|
+
var HS_NEUTRAL_CHIP = "#CBD6E2";
|
|
37
|
+
|
|
38
|
+
// src/common-components/AvatarStack.js
|
|
39
|
+
var DEFAULT_COLORS = [
|
|
40
|
+
"#0091ae",
|
|
41
|
+
"#8B0000",
|
|
42
|
+
"#ff5c35",
|
|
43
|
+
"#00bda5",
|
|
44
|
+
"#fdcc00",
|
|
45
|
+
"#516f90",
|
|
46
|
+
"#003366",
|
|
47
|
+
"#8e7cc3"
|
|
48
|
+
];
|
|
49
|
+
var SIZE_TOKENS = {
|
|
50
|
+
xs: 16,
|
|
51
|
+
"extra-small": 16,
|
|
52
|
+
sm: 20,
|
|
53
|
+
"small": 20,
|
|
54
|
+
md: 24,
|
|
55
|
+
"med": 24,
|
|
56
|
+
"medium": 24,
|
|
57
|
+
lg: 32,
|
|
58
|
+
"large": 32,
|
|
59
|
+
xl: 40,
|
|
60
|
+
"extra-large": 40
|
|
61
|
+
};
|
|
62
|
+
var resolveSize = (size) => {
|
|
63
|
+
if (typeof size === "number") return size;
|
|
64
|
+
if (typeof size === "string" && SIZE_TOKENS[size] != null) return SIZE_TOKENS[size];
|
|
65
|
+
return 24;
|
|
66
|
+
};
|
|
67
|
+
var isImageUri = (s) => typeof s === "string" && /^(https?:|data:image\/)/i.test(s);
|
|
68
|
+
var escapeXmlAttr = (s) => String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
69
|
+
var pickColor = (key, palette, index) => {
|
|
70
|
+
if (!key) return palette[index % palette.length];
|
|
71
|
+
const code = String(key).charCodeAt(0) || 0;
|
|
72
|
+
return palette[(code + index) % palette.length];
|
|
73
|
+
};
|
|
74
|
+
var normalizeEntry = (entry) => {
|
|
75
|
+
if (entry == null) return null;
|
|
76
|
+
if (typeof entry === "string") {
|
|
77
|
+
if (entry.length === 0) return null;
|
|
78
|
+
if (isImageUri(entry)) return { src: entry };
|
|
79
|
+
return { letter: entry.slice(0, 2).toUpperCase() };
|
|
80
|
+
}
|
|
81
|
+
if (typeof entry === "object") {
|
|
82
|
+
if (entry.src) return { src: entry.src, letter: entry.letter };
|
|
83
|
+
if (entry.letter) return { letter: String(entry.letter).slice(0, 2).toUpperCase(), color: entry.color };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
var makeAvatarStackDataUri = (rawEntries, opts = {}) => {
|
|
88
|
+
const {
|
|
89
|
+
size: sizeProp = "medium",
|
|
90
|
+
step: stepProp,
|
|
91
|
+
overlap: overlapProp,
|
|
92
|
+
maxVisible = 4,
|
|
93
|
+
colors = DEFAULT_COLORS,
|
|
94
|
+
overflowBg = HS_NEUTRAL_CHIP,
|
|
95
|
+
overflowColor = HS_TEXT_COLOR,
|
|
96
|
+
fontFamily = HS_FONT_FAMILY
|
|
97
|
+
} = opts;
|
|
98
|
+
const size = resolveSize(sizeProp);
|
|
99
|
+
let step;
|
|
100
|
+
if (stepProp != null) {
|
|
101
|
+
step = stepProp;
|
|
102
|
+
} else if (overlapProp != null) {
|
|
103
|
+
const clampedOverlap = Math.max(0, Math.min(size - 1, overlapProp));
|
|
104
|
+
step = size - clampedOverlap;
|
|
105
|
+
} else {
|
|
106
|
+
step = Math.round(size * 0.65);
|
|
107
|
+
}
|
|
108
|
+
const entries = (rawEntries || []).map(normalizeEntry).filter(Boolean);
|
|
109
|
+
if (entries.length === 0) return null;
|
|
110
|
+
const visible = entries.slice(0, maxVisible);
|
|
111
|
+
const overflowCount = entries.length - visible.length;
|
|
112
|
+
const slots = overflowCount > 0 ? [...entries.slice(0, maxVisible - 1), { overflow: overflowCount }] : visible;
|
|
113
|
+
const count = slots.length;
|
|
114
|
+
const r = size / 2;
|
|
115
|
+
const haloR = r + 1;
|
|
116
|
+
const width = size + (count - 1) * step;
|
|
117
|
+
const height = size;
|
|
118
|
+
const defs = `<defs><clipPath id="hsuixAvatarClip"><circle cx="${r}" cy="${r}" r="${r}"/></clipPath></defs>`;
|
|
119
|
+
const fontFamilyAttr = fontFamily.replace(/"/g, """);
|
|
120
|
+
const pieces = slots.map((slot, i) => {
|
|
121
|
+
const cx = r + i * step;
|
|
122
|
+
const tx = i * step;
|
|
123
|
+
const halo = i > 0 ? `<circle cx="${cx}" cy="${r}" r="${haloR}" fill="#ffffff" />` : "";
|
|
124
|
+
if (slot.overflow) {
|
|
125
|
+
return halo + `<circle cx="${cx}" cy="${r}" r="${r}" fill="${overflowBg}" /><text x="${cx}" y="${r + 1}" text-anchor="middle" dominant-baseline="central" font-family="${fontFamilyAttr}" font-size="${Math.round(size * 0.42)}" font-weight="700" fill="${overflowColor}">+${slot.overflow}</text>`;
|
|
126
|
+
}
|
|
127
|
+
if (slot.src) {
|
|
128
|
+
return halo + `<g transform="translate(${tx}, 0)"><image href="${escapeXmlAttr(slot.src)}" x="0" y="0" width="${size}" height="${size}" preserveAspectRatio="xMidYMid slice" clip-path="url(#hsuixAvatarClip)" /></g>`;
|
|
129
|
+
}
|
|
130
|
+
const letter = slot.letter || "?";
|
|
131
|
+
const bgColor = slot.color || pickColor(letter, colors, i);
|
|
132
|
+
return halo + `<circle cx="${cx}" cy="${r}" r="${r}" fill="${bgColor}" /><text x="${cx}" y="${r + 1}" text-anchor="middle" dominant-baseline="central" font-family="${fontFamilyAttr}" font-size="${Math.round(size * 0.46)}" font-weight="700" fill="#ffffff">${escapeXmlAttr(letter)}</text>`;
|
|
133
|
+
});
|
|
134
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}">` + defs + pieces.join("") + `</svg>`;
|
|
135
|
+
return {
|
|
136
|
+
src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`,
|
|
137
|
+
width,
|
|
138
|
+
height,
|
|
139
|
+
count
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
var AvatarStack = ({
|
|
143
|
+
items,
|
|
144
|
+
size,
|
|
145
|
+
overlap,
|
|
146
|
+
step,
|
|
147
|
+
maxVisible,
|
|
148
|
+
colors,
|
|
149
|
+
overflowBg,
|
|
150
|
+
overflowColor,
|
|
151
|
+
fontFamily,
|
|
152
|
+
alt
|
|
153
|
+
}) => {
|
|
154
|
+
const stack = makeAvatarStackDataUri(items, {
|
|
155
|
+
size,
|
|
156
|
+
overlap,
|
|
157
|
+
step,
|
|
158
|
+
maxVisible,
|
|
159
|
+
colors,
|
|
160
|
+
overflowBg,
|
|
161
|
+
overflowColor,
|
|
162
|
+
fontFamily
|
|
163
|
+
});
|
|
164
|
+
if (!stack) return null;
|
|
165
|
+
return React.createElement(Image, {
|
|
166
|
+
src: stack.src,
|
|
167
|
+
width: stack.width,
|
|
168
|
+
height: stack.height,
|
|
169
|
+
alt: alt ?? `${items.length} associated records`
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// packages/feed/src/Feed.jsx
|
|
174
|
+
var DEFAULT_LABELS = {
|
|
175
|
+
search: "Search activity...",
|
|
176
|
+
sort: "Sort",
|
|
177
|
+
all: "All",
|
|
178
|
+
clearAll: "Clear all",
|
|
179
|
+
dateFrom: "From",
|
|
180
|
+
dateTo: "To",
|
|
181
|
+
loading: "Loading feed...",
|
|
182
|
+
loadingMessage: "This should only take a moment.",
|
|
183
|
+
loadingMore: "Loading...",
|
|
184
|
+
loadMore: "View more",
|
|
185
|
+
collapseAll: "Collapse all",
|
|
186
|
+
expandAll: "Expand all",
|
|
187
|
+
emptyTitle: "No activity yet",
|
|
188
|
+
emptyMessage: "New activity will appear here.",
|
|
189
|
+
errorTitle: "Something went wrong.",
|
|
190
|
+
errorMessage: "An error occurred while loading the feed.",
|
|
191
|
+
itemCount: (shown, total, label) => shown === total ? `${total} ${label}` : `${shown} of ${total} ${label}`,
|
|
192
|
+
allItems: "All"
|
|
193
|
+
};
|
|
194
|
+
var DEFAULT_RECORD_LABEL = { singular: "item", plural: "items" };
|
|
195
|
+
var DEFAULT_SEARCH_FIELDS = ["title", "subject", "body", "description", "content", "preview", "type", "typeLabel", "actorName", "author"];
|
|
196
|
+
var DEFAULT_PAGE_SIZE = 5;
|
|
197
|
+
var hasValue = (value) => value != null && value !== false && value !== "";
|
|
198
|
+
var getItemKey = (item, index, getKey) => {
|
|
199
|
+
if (typeof getKey === "function") return getKey(item, index);
|
|
200
|
+
return (item == null ? void 0 : item.id) ?? (item == null ? void 0 : item.key) ?? `feed-item-${index}`;
|
|
201
|
+
};
|
|
202
|
+
var getValue = (item, field) => {
|
|
203
|
+
if (typeof field === "function") return field(item);
|
|
204
|
+
if (typeof field === "string") return item == null ? void 0 : item[field];
|
|
205
|
+
return void 0;
|
|
206
|
+
};
|
|
207
|
+
var normalizeText = (value) => String(value ?? "").toLowerCase();
|
|
208
|
+
var pickTimestamp = (item) => (item == null ? void 0 : item.timestamp) ?? (item == null ? void 0 : item.time) ?? (item == null ? void 0 : item.date) ?? (item == null ? void 0 : item.createdAt) ?? (item == null ? void 0 : item.dateLabel);
|
|
209
|
+
var pickActor = (item) => (item == null ? void 0 : item.actor) ?? (item == null ? void 0 : item.actorName) ?? (item == null ? void 0 : item.author);
|
|
210
|
+
var pickActorName = (actor) => {
|
|
211
|
+
if (!actor || typeof actor !== "object") return actor;
|
|
212
|
+
return actor.name ?? actor.label ?? actor.email;
|
|
213
|
+
};
|
|
214
|
+
var pickActorAvatar = (actor) => {
|
|
215
|
+
if (!actor || typeof actor !== "object") return null;
|
|
216
|
+
return actor.avatar ?? actor.avatarUrl ?? actor.src ?? actor.initials ?? actor.name;
|
|
217
|
+
};
|
|
218
|
+
var pickBody = (item) => (item == null ? void 0 : item.body) ?? (item == null ? void 0 : item.description) ?? (item == null ? void 0 : item.content) ?? (item == null ? void 0 : item.preview) ?? (item == null ? void 0 : item.notePreview);
|
|
219
|
+
var pickHeaderActions = (item) => item == null ? void 0 : item.headerActions;
|
|
220
|
+
var itemHasExpandableContent = (item, fields) => {
|
|
221
|
+
if ((item == null ? void 0 : item.collapsible) === false) return false;
|
|
222
|
+
if ((item == null ? void 0 : item.collapsible) === true) return true;
|
|
223
|
+
if (hasValue(pickBody(item))) return true;
|
|
224
|
+
if (hasValue(pickActor(item))) return true;
|
|
225
|
+
if (hasValue(item == null ? void 0 : item.actions)) return true;
|
|
226
|
+
if (hasValue(item == null ? void 0 : item.footer)) return true;
|
|
227
|
+
if (hasValue(item == null ? void 0 : item.meta) || hasValue(item == null ? void 0 : item.metadata)) return true;
|
|
228
|
+
if (Array.isArray(fields)) {
|
|
229
|
+
if (fields.some((f) => ["body", "footer"].includes(f.placement ?? "body"))) return true;
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
};
|
|
233
|
+
var pickMeta = (item) => (item == null ? void 0 : item.meta) ?? (item == null ? void 0 : item.metadata);
|
|
234
|
+
var pickType = (item) => (item == null ? void 0 : item.typeLabel) ?? (item == null ? void 0 : item.type);
|
|
235
|
+
var pickStatus = (item) => (item == null ? void 0 : item.statusLabel) ?? (item == null ? void 0 : item.status) ?? (item == null ? void 0 : item.outcome) ?? (item == null ? void 0 : item.severity);
|
|
236
|
+
var pickStatusVariant = (item) => (item == null ? void 0 : item.statusVariant) ?? (item == null ? void 0 : item.outcomeVariant) ?? (item == null ? void 0 : item.severityVariant) ?? "default";
|
|
237
|
+
var toDate = (value) => {
|
|
238
|
+
if (!value) return null;
|
|
239
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
240
|
+
if (typeof value === "object" && value.year != null && value.month != null && value.date != null) {
|
|
241
|
+
return new Date(value.year, value.month, value.date);
|
|
242
|
+
}
|
|
243
|
+
const parsed = new Date(value);
|
|
244
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
245
|
+
};
|
|
246
|
+
var compareValues = (a, b) => {
|
|
247
|
+
const aDate = toDate(a);
|
|
248
|
+
const bDate = toDate(b);
|
|
249
|
+
if (aDate && bDate) return aDate.getTime() - bDate.getTime();
|
|
250
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
251
|
+
return String(a ?? "").localeCompare(String(b ?? ""));
|
|
252
|
+
};
|
|
253
|
+
var formatDateGroup = (value) => {
|
|
254
|
+
if (!value) return "No date";
|
|
255
|
+
const date = toDate(value);
|
|
256
|
+
if (!date) return String(value);
|
|
257
|
+
const today = /* @__PURE__ */ new Date();
|
|
258
|
+
const yesterday = /* @__PURE__ */ new Date();
|
|
259
|
+
yesterday.setDate(today.getDate() - 1);
|
|
260
|
+
const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
261
|
+
if (sameDay(date, today)) return "Today";
|
|
262
|
+
if (sameDay(date, yesterday)) return "Yesterday";
|
|
263
|
+
return date.toLocaleDateString(void 0, {
|
|
264
|
+
month: "short",
|
|
265
|
+
day: "numeric",
|
|
266
|
+
year: date.getFullYear() === today.getFullYear() ? void 0 : "numeric"
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
var formatTimestamp = (value) => {
|
|
270
|
+
if (!value) return value;
|
|
271
|
+
const date = toDate(value);
|
|
272
|
+
if (!date) return value;
|
|
273
|
+
return date.toLocaleString(void 0, {
|
|
274
|
+
month: "short",
|
|
275
|
+
day: "numeric",
|
|
276
|
+
year: date.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear() ? void 0 : "numeric",
|
|
277
|
+
hour: "numeric",
|
|
278
|
+
minute: "2-digit"
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
var renderMaybe = (renderer, item, index) => {
|
|
282
|
+
if (typeof renderer !== "function") return null;
|
|
283
|
+
return renderer(item, index);
|
|
284
|
+
};
|
|
285
|
+
var isEmptyFilterValue = (value) => {
|
|
286
|
+
if (value == null || value === "" || value === "all") return true;
|
|
287
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
288
|
+
if (typeof value === "object") return !value.from && !value.to;
|
|
289
|
+
return false;
|
|
290
|
+
};
|
|
291
|
+
var getFilterValue = (filterValues, filter) => {
|
|
292
|
+
if (!filterValues) return filter.defaultValue;
|
|
293
|
+
const value = filterValues[filter.name];
|
|
294
|
+
return value === void 0 ? filter.defaultValue : value;
|
|
295
|
+
};
|
|
296
|
+
var getRecordLabel = (recordLabel, count) => {
|
|
297
|
+
const label = { ...DEFAULT_RECORD_LABEL, ...recordLabel || {} };
|
|
298
|
+
return count === 1 ? label.singular : label.plural;
|
|
299
|
+
};
|
|
300
|
+
var FeedIcon = ({ name, size = "sm" }) => {
|
|
301
|
+
if (!name) return null;
|
|
302
|
+
return /* @__PURE__ */ React2.createElement(Icon, { name, size, purpose: "decorative" });
|
|
303
|
+
};
|
|
304
|
+
var FeedActorAvatar = ({ item, avatarSize }) => {
|
|
305
|
+
const actor = pickActor(item);
|
|
306
|
+
const avatar = (item == null ? void 0 : item.avatar) ?? pickActorAvatar(actor);
|
|
307
|
+
if (!hasValue(avatar)) return null;
|
|
308
|
+
return /* @__PURE__ */ React2.createElement(
|
|
309
|
+
AvatarStack,
|
|
310
|
+
{
|
|
311
|
+
items: [avatar],
|
|
312
|
+
size: item.avatarSize ?? avatarSize,
|
|
313
|
+
maxVisible: 1,
|
|
314
|
+
alt: item.avatarAlt ?? pickActorName(actor) ?? item.title ?? "Feed item avatar"
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
var FeedTypeIcon = ({ item, iconSize }) => {
|
|
319
|
+
if (hasValue(item == null ? void 0 : item.icon) && typeof item.icon !== "string") return item.icon;
|
|
320
|
+
const iconName = typeof (item == null ? void 0 : item.icon) === "string" ? item.icon : item == null ? void 0 : item.iconName;
|
|
321
|
+
if (!hasValue(iconName)) return null;
|
|
322
|
+
return /* @__PURE__ */ React2.createElement(Icon, { name: iconName, size: iconSize, purpose: "decorative" });
|
|
323
|
+
};
|
|
324
|
+
var FeedActions = ({ actions }) => {
|
|
325
|
+
if (!Array.isArray(actions) || actions.length === 0) return actions || null;
|
|
326
|
+
return /* @__PURE__ */ React2.createElement(ButtonRow, { dropDownButtonOptions: { text: "More", size: "small", variant: "transparent" } }, actions.filter(Boolean).slice(0, 3).map((action, index) => /* @__PURE__ */ React2.createElement(
|
|
327
|
+
Button,
|
|
328
|
+
{
|
|
329
|
+
key: action.key ?? action.label ?? `action-${index}`,
|
|
330
|
+
size: action.size ?? "small",
|
|
331
|
+
variant: action.variant ?? "transparent",
|
|
332
|
+
disabled: action.disabled,
|
|
333
|
+
href: action.href,
|
|
334
|
+
onClick: action.onClick
|
|
335
|
+
},
|
|
336
|
+
action.icon ? /* @__PURE__ */ React2.createElement(Icon, { name: action.icon, size: action.iconSize ?? "sm", purpose: "decorative" }) : null,
|
|
337
|
+
action.label
|
|
338
|
+
)));
|
|
339
|
+
};
|
|
340
|
+
var FeedField = ({ field, item, index }) => {
|
|
341
|
+
if (!field) return null;
|
|
342
|
+
if (field.visible && !field.visible(item)) return null;
|
|
343
|
+
const value = getValue(item, field.field);
|
|
344
|
+
const rendered = field.render ? field.render(value, item, index) : value;
|
|
345
|
+
if (!hasValue(rendered)) return null;
|
|
346
|
+
if (field.href) {
|
|
347
|
+
const href = typeof field.href === "function" ? field.href(item) : field.href;
|
|
348
|
+
return /* @__PURE__ */ React2.createElement(Link, { href }, rendered);
|
|
349
|
+
}
|
|
350
|
+
if (field.type === "status") {
|
|
351
|
+
const variant = typeof field.variant === "function" ? field.variant(value, item) : field.variant;
|
|
352
|
+
return /* @__PURE__ */ React2.createElement(StatusTag, { variant: variant ?? "default" }, rendered);
|
|
353
|
+
}
|
|
354
|
+
if (field.type === "tag") {
|
|
355
|
+
const variant = typeof field.variant === "function" ? field.variant(value, item) : field.variant;
|
|
356
|
+
return /* @__PURE__ */ React2.createElement(Tag, { variant: variant ?? "default" }, rendered);
|
|
357
|
+
}
|
|
358
|
+
if (field.label) {
|
|
359
|
+
return /* @__PURE__ */ React2.createElement(DescriptionListItem, { label: field.label }, rendered);
|
|
360
|
+
}
|
|
361
|
+
return rendered;
|
|
362
|
+
};
|
|
363
|
+
var fieldsForPlacement = (fields, placement) => Array.isArray(fields) ? fields.filter((field) => (field.placement ?? "body") === placement) : [];
|
|
364
|
+
var renderPlacedFields = ({ fields, placement, item, index, inline = false }) => {
|
|
365
|
+
const placed = fieldsForPlacement(fields, placement);
|
|
366
|
+
if (placed.length === 0) return null;
|
|
367
|
+
const nodes = placed.map((field, fieldIndex) => /* @__PURE__ */ React2.createElement(
|
|
368
|
+
FeedField,
|
|
369
|
+
{
|
|
370
|
+
key: field.key ?? field.field ?? `${placement}-${fieldIndex}`,
|
|
371
|
+
field,
|
|
372
|
+
item,
|
|
373
|
+
index
|
|
374
|
+
}
|
|
375
|
+
)).filter(Boolean);
|
|
376
|
+
if (nodes.length === 0) return null;
|
|
377
|
+
if (inline) return /* @__PURE__ */ React2.createElement(Inline, { gap: "xs", align: "center" }, nodes);
|
|
378
|
+
if (placed.some((field) => field.label)) return /* @__PURE__ */ React2.createElement(DescriptionList, { direction: "row" }, nodes);
|
|
379
|
+
return /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: "xs" }, nodes);
|
|
380
|
+
};
|
|
381
|
+
var renderHeaderActions = (headerActions) => {
|
|
382
|
+
if (!hasValue(headerActions)) return null;
|
|
383
|
+
if (!Array.isArray(headerActions)) return headerActions;
|
|
384
|
+
return /* @__PURE__ */ React2.createElement(Inline, { gap: "sm", align: "center" }, headerActions.filter(Boolean).map((action, index) => /* @__PURE__ */ React2.createElement(
|
|
385
|
+
Link,
|
|
386
|
+
{
|
|
387
|
+
key: action.key ?? action.label ?? `header-action-${index}`,
|
|
388
|
+
href: action.href,
|
|
389
|
+
onClick: action.onClick
|
|
390
|
+
},
|
|
391
|
+
action.label
|
|
392
|
+
)));
|
|
393
|
+
};
|
|
394
|
+
var DefaultFeedItem = ({
|
|
395
|
+
item,
|
|
396
|
+
index,
|
|
397
|
+
fields,
|
|
398
|
+
compact,
|
|
399
|
+
avatarSize,
|
|
400
|
+
iconSize,
|
|
401
|
+
collapsible,
|
|
402
|
+
expanded,
|
|
403
|
+
onToggleExpanded,
|
|
404
|
+
renderActor,
|
|
405
|
+
renderTimestamp,
|
|
406
|
+
renderMeta,
|
|
407
|
+
renderActions,
|
|
408
|
+
renderFooter
|
|
409
|
+
}) => {
|
|
410
|
+
const rawActor = pickActor(item);
|
|
411
|
+
const actor = renderMaybe(renderActor, item, index) ?? pickActorName(rawActor);
|
|
412
|
+
const timestamp = renderMaybe(renderTimestamp, item, index) ?? pickTimestamp(item);
|
|
413
|
+
const meta = renderMaybe(renderMeta, item, index) ?? pickMeta(item);
|
|
414
|
+
const type = pickType(item);
|
|
415
|
+
const status = pickStatus(item);
|
|
416
|
+
const statusVariant = pickStatusVariant(item);
|
|
417
|
+
const actions = renderMaybe(renderActions, item, index) ?? (item == null ? void 0 : item.actions);
|
|
418
|
+
const headerActions = pickHeaderActions(item);
|
|
419
|
+
const footer = renderMaybe(renderFooter, item, index) ?? (item == null ? void 0 : item.footer);
|
|
420
|
+
const body = pickBody(item);
|
|
421
|
+
const avatar = /* @__PURE__ */ React2.createElement(FeedActorAvatar, { item, avatarSize });
|
|
422
|
+
const typeIcon = /* @__PURE__ */ React2.createElement(FeedTypeIcon, { item, iconSize });
|
|
423
|
+
const hasAvatarNode = hasValue(item == null ? void 0 : item.avatar) || hasValue(pickActorAvatar(rawActor));
|
|
424
|
+
const titleFields = fieldsForPlacement(fields, "title");
|
|
425
|
+
const titleField = titleFields.length > 0 ? /* @__PURE__ */ React2.createElement(FeedField, { field: titleFields[0], item, index }) : null;
|
|
426
|
+
const subtitleFields = renderPlacedFields({ fields, placement: "subtitle", item, index, inline: true });
|
|
427
|
+
const metaFields = renderPlacedFields({ fields, placement: "meta", item, index, inline: true });
|
|
428
|
+
const bodyFields = renderPlacedFields({ fields, placement: "body", item, index });
|
|
429
|
+
const footerFields = renderPlacedFields({ fields, placement: "footer", item, index, inline: true });
|
|
430
|
+
const titleContent = titleField ?? (item == null ? void 0 : item.title) ?? (item == null ? void 0 : item.subject);
|
|
431
|
+
const title = hasValue(item == null ? void 0 : item.href) ? /* @__PURE__ */ React2.createElement(Link, { href: item.href }, titleContent) : titleContent;
|
|
432
|
+
const headerLeft = /* @__PURE__ */ React2.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, collapsible ? /* @__PURE__ */ React2.createElement(Link, { onClick: onToggleExpanded }, /* @__PURE__ */ React2.createElement(Icon, { name: expanded ? "downCarat" : "right", size: "sm", purpose: "decorative" })) : null, typeIcon, hasValue(title) && /* @__PURE__ */ React2.createElement(Text, { format: { fontWeight: "demibold" }, truncate: true }, title));
|
|
433
|
+
const headerRight = hasValue(headerActions) || hasValue(timestamp) || hasValue(type) ? /* @__PURE__ */ React2.createElement(Inline, { gap: "sm", align: "center" }, renderHeaderActions(headerActions), hasValue(type) && /* @__PURE__ */ React2.createElement(Text, { variant: "microcopy" }, type), hasValue(timestamp) && /* @__PURE__ */ React2.createElement(Text, { variant: "microcopy" }, formatTimestamp(timestamp))) : null;
|
|
434
|
+
const showBody = !collapsible || expanded;
|
|
435
|
+
return /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: compact ? "xs" : "sm" }, /* @__PURE__ */ React2.createElement(Flex, { direction: "row", justify: "between", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ React2.createElement(Box, { flex: 1 }, headerLeft), headerRight), showBody ? /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: compact ? "xs" : "sm" }, (hasAvatarNode || hasValue(actor) || hasValue(subtitleFields) || hasValue(status)) && /* @__PURE__ */ React2.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "wrap" }, hasAvatarNode ? avatar : null, hasValue(actor) && /* @__PURE__ */ React2.createElement(Text, { variant: "microcopy" }, actor), subtitleFields, hasValue(status) && /* @__PURE__ */ React2.createElement(StatusTag, { variant: statusVariant, hollow: true }, status)), hasValue(body) && /* @__PURE__ */ React2.createElement(Text, null, body), bodyFields, Array.isArray(meta) ? meta.length > 0 ? /* @__PURE__ */ React2.createElement(List, { variant: "inline-divided" }, meta) : metaFields : hasValue(meta) ? /* @__PURE__ */ React2.createElement(Inline, { gap: "xs" }, meta) : metaFields ? metaFields : null, hasValue(actions) && /* @__PURE__ */ React2.createElement(FeedActions, { actions }), (hasValue(footer) || hasValue(footerFields)) && /* @__PURE__ */ React2.createElement(Inline, { gap: "xs", align: "center" }, footerFields, footer)) : null);
|
|
436
|
+
};
|
|
437
|
+
var applyTab = (items, activeTab, tabField) => {
|
|
438
|
+
if (!activeTab || activeTab === "all") return items;
|
|
439
|
+
return items.filter((item) => String(getValue(item, tabField)) === String(activeTab));
|
|
440
|
+
};
|
|
441
|
+
var normalizeTabs = (tabs, items, tabField, labels) => {
|
|
442
|
+
if (Array.isArray(tabs) && tabs.length > 0) return tabs;
|
|
443
|
+
const values = [];
|
|
444
|
+
items.forEach((item) => {
|
|
445
|
+
const value = getValue(item, tabField);
|
|
446
|
+
if (value != null && value !== "" && !values.some((v) => String(v) === String(value))) values.push(value);
|
|
447
|
+
});
|
|
448
|
+
return [
|
|
449
|
+
{ label: labels.allItems, value: "all" },
|
|
450
|
+
...values.map((value) => ({ label: String(value), value }))
|
|
451
|
+
];
|
|
452
|
+
};
|
|
453
|
+
var applySearch = (items, search, searchFields) => {
|
|
454
|
+
const term = normalizeText(search).trim();
|
|
455
|
+
if (!term) return items;
|
|
456
|
+
const fields = Array.isArray(searchFields) && searchFields.length > 0 ? searchFields : DEFAULT_SEARCH_FIELDS;
|
|
457
|
+
return items.filter((item) => fields.some((field) => {
|
|
458
|
+
const value = field === "actor" || field === "author" ? pickActorName(getValue(item, field)) : getValue(item, field);
|
|
459
|
+
return normalizeText(value).includes(term);
|
|
460
|
+
}));
|
|
461
|
+
};
|
|
462
|
+
var applyFilters = (items, filters, filterValues) => {
|
|
463
|
+
if (!Array.isArray(filters) || filters.length === 0) return items;
|
|
464
|
+
return items.filter((item) => filters.every((filter) => {
|
|
465
|
+
const value = getFilterValue(filterValues, filter);
|
|
466
|
+
if (isEmptyFilterValue(value)) return true;
|
|
467
|
+
if (typeof filter.filterFn === "function") return filter.filterFn(item, value);
|
|
468
|
+
const itemValue = getValue(item, filter.field ?? filter.name);
|
|
469
|
+
if (filter.type === "multiselect") {
|
|
470
|
+
const selected = Array.isArray(value) ? value : [value];
|
|
471
|
+
return selected.map(String).includes(String(itemValue));
|
|
472
|
+
}
|
|
473
|
+
if (filter.type === "dateRange") {
|
|
474
|
+
const itemDate = toDate(itemValue);
|
|
475
|
+
if (!itemDate) return false;
|
|
476
|
+
const from = toDate(value.from);
|
|
477
|
+
const to = toDate(value.to);
|
|
478
|
+
if (from && itemDate < from) return false;
|
|
479
|
+
if (to && itemDate > to) return false;
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
return String(itemValue) === String(value);
|
|
483
|
+
}));
|
|
484
|
+
};
|
|
485
|
+
var applySort = (items, sortValue, sortOptions) => {
|
|
486
|
+
if (!sortValue || !Array.isArray(sortOptions) || sortOptions.length === 0) return items;
|
|
487
|
+
const option = sortOptions.find((opt) => opt.value === sortValue);
|
|
488
|
+
if (!option) return items;
|
|
489
|
+
const sorted = [...items];
|
|
490
|
+
sorted.sort((a, b) => {
|
|
491
|
+
if (typeof option.comparator === "function") return option.comparator(a, b);
|
|
492
|
+
const direction = option.direction === "desc" || option.direction === "descending" ? -1 : 1;
|
|
493
|
+
return compareValues(getValue(a, option.field), getValue(b, option.field)) * direction;
|
|
494
|
+
});
|
|
495
|
+
return sorted;
|
|
496
|
+
};
|
|
497
|
+
var buildGroups = ({ items, groupBy, groupByDate, getGroupLabel }) => {
|
|
498
|
+
if (!groupBy && !groupByDate) return [{ key: "__all__", label: null, items }];
|
|
499
|
+
const groups = [];
|
|
500
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
501
|
+
items.forEach((item) => {
|
|
502
|
+
const rawKey = groupByDate ? formatDateGroup(pickTimestamp(item)) : getValue(item, groupBy);
|
|
503
|
+
const key = rawKey == null ? "__empty__" : String(rawKey);
|
|
504
|
+
const label = typeof getGroupLabel === "function" ? getGroupLabel(rawKey, item) : key === "__empty__" ? "Other" : key;
|
|
505
|
+
if (!groupMap.has(key)) {
|
|
506
|
+
const group = { key, label, items: [] };
|
|
507
|
+
groupMap.set(key, group);
|
|
508
|
+
groups.push(group);
|
|
509
|
+
}
|
|
510
|
+
groupMap.get(key).items.push(item);
|
|
511
|
+
});
|
|
512
|
+
return groups;
|
|
513
|
+
};
|
|
514
|
+
var FeedToolbar = ({
|
|
515
|
+
labels,
|
|
516
|
+
search,
|
|
517
|
+
onSearch,
|
|
518
|
+
showSearch,
|
|
519
|
+
searchPlaceholder,
|
|
520
|
+
filters,
|
|
521
|
+
filterValues,
|
|
522
|
+
onFilter,
|
|
523
|
+
filterInlineLimit,
|
|
524
|
+
sortOptions,
|
|
525
|
+
sort,
|
|
526
|
+
onSort,
|
|
527
|
+
showItemCount,
|
|
528
|
+
itemCountLabel
|
|
529
|
+
}) => {
|
|
530
|
+
const [showMoreFilters, setShowMoreFilters] = useState(false);
|
|
531
|
+
const inlineFilters = (filters || []).slice(0, filterInlineLimit);
|
|
532
|
+
const overflowFilters = (filters || []).slice(filterInlineLimit);
|
|
533
|
+
const renderFilterControl = (filter) => {
|
|
534
|
+
const value = getFilterValue(filterValues, filter);
|
|
535
|
+
if (filter.type === "multiselect") {
|
|
536
|
+
return /* @__PURE__ */ React2.createElement(
|
|
537
|
+
MultiSelect,
|
|
538
|
+
{
|
|
539
|
+
key: filter.name,
|
|
540
|
+
name: `feed-filter-${filter.name}`,
|
|
541
|
+
label: "",
|
|
542
|
+
placeholder: filter.placeholder || filter.label || labels.all,
|
|
543
|
+
value: Array.isArray(value) ? value : [],
|
|
544
|
+
options: filter.options || [],
|
|
545
|
+
onChange: (next) => onFilter(filter.name, next)
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (filter.type === "dateRange") {
|
|
550
|
+
return /* @__PURE__ */ React2.createElement(Flex, { key: filter.name, direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React2.createElement(
|
|
551
|
+
DateInput,
|
|
552
|
+
{
|
|
553
|
+
label: "",
|
|
554
|
+
placeholder: filter.fromLabel ?? labels.dateFrom,
|
|
555
|
+
name: `feed-filter-${filter.name}-from`,
|
|
556
|
+
format: "medium",
|
|
557
|
+
value: (value == null ? void 0 : value.from) ?? null,
|
|
558
|
+
onChange: (next) => onFilter(filter.name, { ...value || {}, from: next })
|
|
559
|
+
}
|
|
560
|
+
), /* @__PURE__ */ React2.createElement(FeedIcon, { name: "dataSync", size: "sm" }), /* @__PURE__ */ React2.createElement(
|
|
561
|
+
DateInput,
|
|
562
|
+
{
|
|
563
|
+
label: "",
|
|
564
|
+
placeholder: filter.toLabel ?? labels.dateTo,
|
|
565
|
+
name: `feed-filter-${filter.name}-to`,
|
|
566
|
+
format: "medium",
|
|
567
|
+
value: (value == null ? void 0 : value.to) ?? null,
|
|
568
|
+
onChange: (next) => onFilter(filter.name, { ...value || {}, to: next })
|
|
569
|
+
}
|
|
570
|
+
));
|
|
571
|
+
}
|
|
572
|
+
const options = filter.includeAll === false ? filter.options || [] : [{ label: filter.allLabel ?? filter.placeholder ?? filter.label ?? labels.all, value: "all" }, ...filter.options || []];
|
|
573
|
+
return /* @__PURE__ */ React2.createElement(
|
|
574
|
+
Select,
|
|
575
|
+
{
|
|
576
|
+
key: filter.name,
|
|
577
|
+
name: `feed-filter-${filter.name}`,
|
|
578
|
+
variant: "transparent",
|
|
579
|
+
placeholder: filter.placeholder || filter.label || labels.all,
|
|
580
|
+
value: value ?? "all",
|
|
581
|
+
options,
|
|
582
|
+
onChange: (next) => onFilter(filter.name, next)
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
const hasLeftControls = showSearch || inlineFilters.length > 0 || overflowFilters.length > 0;
|
|
587
|
+
const hasRightControls = Array.isArray(sortOptions) && sortOptions.length > 0 || showItemCount;
|
|
588
|
+
if (!hasLeftControls && !hasRightControls) return null;
|
|
589
|
+
return /* @__PURE__ */ React2.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React2.createElement(Box, { flex: 3 }, /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: "sm" }, hasLeftControls ? /* @__PURE__ */ React2.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch ? /* @__PURE__ */ React2.createElement(
|
|
590
|
+
SearchInput,
|
|
591
|
+
{
|
|
592
|
+
name: "feed-search",
|
|
593
|
+
placeholder: searchPlaceholder ?? labels.search,
|
|
594
|
+
value: search,
|
|
595
|
+
clearable: true,
|
|
596
|
+
onChange: onSearch
|
|
597
|
+
}
|
|
598
|
+
) : null, inlineFilters.map(renderFilterControl), overflowFilters.length > 0 ? /* @__PURE__ */ React2.createElement(
|
|
599
|
+
Button,
|
|
600
|
+
{
|
|
601
|
+
variant: "transparent",
|
|
602
|
+
size: "sm",
|
|
603
|
+
onClick: () => setShowMoreFilters((prev) => !prev)
|
|
604
|
+
},
|
|
605
|
+
/* @__PURE__ */ React2.createElement(Icon, { name: "filter", size: "sm", purpose: "decorative" }),
|
|
606
|
+
"Filters"
|
|
607
|
+
) : null) : null, showMoreFilters && overflowFilters.length > 0 ? /* @__PURE__ */ React2.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, overflowFilters.map(renderFilterControl)) : null)), hasRightControls ? /* @__PURE__ */ React2.createElement(Box, { flex: 1, alignSelf: "start" }, /* @__PURE__ */ React2.createElement(Flex, { direction: "row", align: "center", gap: "sm", justify: "end", wrap: "wrap" }, Array.isArray(sortOptions) && sortOptions.length > 0 ? /* @__PURE__ */ React2.createElement(
|
|
608
|
+
Select,
|
|
609
|
+
{
|
|
610
|
+
name: "feed-sort",
|
|
611
|
+
value: sort ?? "",
|
|
612
|
+
variant: "transparent",
|
|
613
|
+
placeholder: labels.sort,
|
|
614
|
+
options: [{ label: labels.sort, value: "" }, ...sortOptions.map((opt) => ({ label: opt.label, value: opt.value }))],
|
|
615
|
+
onChange: onSort
|
|
616
|
+
}
|
|
617
|
+
) : null, showItemCount ? /* @__PURE__ */ React2.createElement(Text, { variant: "microcopy" }, itemCountLabel) : null)) : null);
|
|
618
|
+
};
|
|
619
|
+
var Feed = ({
|
|
620
|
+
items = [],
|
|
621
|
+
fields,
|
|
622
|
+
title,
|
|
623
|
+
description,
|
|
624
|
+
actions,
|
|
625
|
+
children,
|
|
626
|
+
loading = false,
|
|
627
|
+
error = null,
|
|
628
|
+
labels: labelOverrides,
|
|
629
|
+
renderItem,
|
|
630
|
+
renderActor,
|
|
631
|
+
renderTimestamp,
|
|
632
|
+
renderMeta,
|
|
633
|
+
renderActions,
|
|
634
|
+
renderFooter,
|
|
635
|
+
renderToolbar,
|
|
636
|
+
renderEmptyState,
|
|
637
|
+
renderLoadingState,
|
|
638
|
+
renderErrorState,
|
|
639
|
+
getKey,
|
|
640
|
+
groupBy,
|
|
641
|
+
groupByDate = false,
|
|
642
|
+
getGroupLabel,
|
|
643
|
+
bordered = true,
|
|
644
|
+
container = bordered ? "tile" : "none",
|
|
645
|
+
showDividers = true,
|
|
646
|
+
itemContainer = "tile",
|
|
647
|
+
compact = false,
|
|
648
|
+
gap = "sm",
|
|
649
|
+
avatarSize = "small",
|
|
650
|
+
iconSize = "sm",
|
|
651
|
+
tabs,
|
|
652
|
+
showTabs,
|
|
653
|
+
tabField = "type",
|
|
654
|
+
tabVariant = "default",
|
|
655
|
+
tabValue,
|
|
656
|
+
defaultTab = "all",
|
|
657
|
+
onTabChange,
|
|
658
|
+
searchFields = DEFAULT_SEARCH_FIELDS,
|
|
659
|
+
showSearch,
|
|
660
|
+
searchPlaceholder,
|
|
661
|
+
searchValue,
|
|
662
|
+
onSearchChange,
|
|
663
|
+
filters = [],
|
|
664
|
+
filterValues,
|
|
665
|
+
defaultFilterValues = {},
|
|
666
|
+
onFilterChange,
|
|
667
|
+
sortOptions = [],
|
|
668
|
+
sort,
|
|
669
|
+
defaultSort = null,
|
|
670
|
+
onSortChange,
|
|
671
|
+
onParamsChange,
|
|
672
|
+
serverSide = false,
|
|
673
|
+
showToolbar = true,
|
|
674
|
+
showItemCount = true,
|
|
675
|
+
itemCountText,
|
|
676
|
+
recordLabel,
|
|
677
|
+
filterInlineLimit = 1,
|
|
678
|
+
pageSize = DEFAULT_PAGE_SIZE,
|
|
679
|
+
initialItemCount,
|
|
680
|
+
maxItems,
|
|
681
|
+
hasMore = false,
|
|
682
|
+
loadingMore = false,
|
|
683
|
+
onLoadMore,
|
|
684
|
+
collapsible = "auto",
|
|
685
|
+
defaultCollapsed = "none",
|
|
686
|
+
collapsedIds,
|
|
687
|
+
onCollapsedIdsChange,
|
|
688
|
+
showCollapseToggle = true
|
|
689
|
+
}) => {
|
|
690
|
+
const labels = { ...DEFAULT_LABELS, ...labelOverrides || {} };
|
|
691
|
+
const safeItems = Array.isArray(items) ? items : [];
|
|
692
|
+
const [internalTab, setInternalTab] = useState(tabValue ?? defaultTab ?? "all");
|
|
693
|
+
const [internalSearch, setInternalSearch] = useState(searchValue ?? "");
|
|
694
|
+
const [internalFilters, setInternalFilters] = useState(filterValues ?? defaultFilterValues);
|
|
695
|
+
const [internalSort, setInternalSort] = useState(sort ?? defaultSort ?? null);
|
|
696
|
+
const [visibleCount, setVisibleCount] = useState(initialItemCount ?? pageSize);
|
|
697
|
+
const computeInitialCollapsed = () => {
|
|
698
|
+
if (Array.isArray(collapsedIds)) return collapsedIds;
|
|
699
|
+
if (Array.isArray(defaultCollapsed)) return defaultCollapsed;
|
|
700
|
+
if (defaultCollapsed === "all") return safeItems.map((item, idx) => getItemKey(item, idx, getKey));
|
|
701
|
+
return [];
|
|
702
|
+
};
|
|
703
|
+
const [internalCollapsedIds, setInternalCollapsedIds] = useState(computeInitialCollapsed);
|
|
704
|
+
const activeTab = tabValue !== void 0 ? tabValue : internalTab;
|
|
705
|
+
const activeSearch = searchValue !== void 0 ? searchValue : internalSearch;
|
|
706
|
+
const activeFilters = filterValues !== void 0 ? filterValues : internalFilters;
|
|
707
|
+
const activeSort = sort !== void 0 ? sort : internalSort;
|
|
708
|
+
const activeCollapsedIds = Array.isArray(collapsedIds) ? collapsedIds : internalCollapsedIds;
|
|
709
|
+
const resolvedShowSearch = showSearch ?? (Array.isArray(searchFields) && searchFields.length > 0);
|
|
710
|
+
const resolvedMaxItems = maxItems ?? visibleCount;
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (tabValue !== void 0) setInternalTab(tabValue);
|
|
713
|
+
}, [tabValue]);
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
if (searchValue !== void 0) setInternalSearch(searchValue);
|
|
716
|
+
}, [searchValue]);
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
if (filterValues !== void 0) setInternalFilters(filterValues);
|
|
719
|
+
}, [filterValues]);
|
|
720
|
+
useEffect(() => {
|
|
721
|
+
if (sort !== void 0) setInternalSort(sort);
|
|
722
|
+
}, [sort]);
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
if (Array.isArray(collapsedIds)) setInternalCollapsedIds(collapsedIds);
|
|
725
|
+
}, [collapsedIds]);
|
|
726
|
+
const emitParamsChange = (next) => {
|
|
727
|
+
if (typeof onParamsChange === "function") {
|
|
728
|
+
onParamsChange({ tab: activeTab, search: activeSearch, filters: activeFilters, sort: activeSort, ...next });
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
const handleTabChange = (next) => {
|
|
732
|
+
if (tabValue === void 0) setInternalTab(next);
|
|
733
|
+
setVisibleCount(initialItemCount ?? pageSize);
|
|
734
|
+
onTabChange == null ? void 0 : onTabChange(next);
|
|
735
|
+
emitParamsChange({ tab: next });
|
|
736
|
+
};
|
|
737
|
+
const handleSearchChange = (next) => {
|
|
738
|
+
if (searchValue === void 0) setInternalSearch(next);
|
|
739
|
+
setVisibleCount(initialItemCount ?? pageSize);
|
|
740
|
+
onSearchChange == null ? void 0 : onSearchChange(next);
|
|
741
|
+
emitParamsChange({ search: next });
|
|
742
|
+
};
|
|
743
|
+
const handleFilterChange = (name, value) => {
|
|
744
|
+
const nextFilters = { ...activeFilters || {}, [name]: value };
|
|
745
|
+
if (filterValues === void 0) setInternalFilters(nextFilters);
|
|
746
|
+
setVisibleCount(initialItemCount ?? pageSize);
|
|
747
|
+
onFilterChange == null ? void 0 : onFilterChange(nextFilters);
|
|
748
|
+
emitParamsChange({ filters: nextFilters });
|
|
749
|
+
};
|
|
750
|
+
const handleSortChange = (next) => {
|
|
751
|
+
const nextSort = next || null;
|
|
752
|
+
if (sort === void 0) setInternalSort(nextSort);
|
|
753
|
+
setVisibleCount(initialItemCount ?? pageSize);
|
|
754
|
+
onSortChange == null ? void 0 : onSortChange(nextSort);
|
|
755
|
+
emitParamsChange({ sort: nextSort });
|
|
756
|
+
};
|
|
757
|
+
const setCollapsedIds = (next) => {
|
|
758
|
+
if (collapsedIds === void 0) setInternalCollapsedIds(next);
|
|
759
|
+
onCollapsedIdsChange == null ? void 0 : onCollapsedIdsChange(next);
|
|
760
|
+
};
|
|
761
|
+
const toggleItemExpanded = (key) => {
|
|
762
|
+
const isCollapsed = activeCollapsedIds.includes(key);
|
|
763
|
+
setCollapsedIds(isCollapsed ? activeCollapsedIds.filter((id) => id !== key) : [...activeCollapsedIds, key]);
|
|
764
|
+
};
|
|
765
|
+
const handleCollapseAll = () => {
|
|
766
|
+
const collapsibleKeys = visibleItems.map((item, idx) => ({ item, idx })).filter(({ item }) => itemHasExpandableContent(item, fields)).map(({ item, idx }) => getItemKey(item, idx, getKey));
|
|
767
|
+
setCollapsedIds(collapsibleKeys);
|
|
768
|
+
};
|
|
769
|
+
const handleExpandAll = () => setCollapsedIds([]);
|
|
770
|
+
const processedItems = useMemo(() => {
|
|
771
|
+
if (serverSide) return safeItems;
|
|
772
|
+
const tabbed = applyTab(safeItems, activeTab, tabField);
|
|
773
|
+
const searched = applySearch(tabbed, activeSearch, searchFields);
|
|
774
|
+
const filtered = applyFilters(searched, filters, activeFilters);
|
|
775
|
+
return applySort(filtered, activeSort, sortOptions);
|
|
776
|
+
}, [safeItems, activeTab, tabField, activeSearch, searchFields, filters, activeFilters, activeSort, sortOptions, serverSide]);
|
|
777
|
+
const visibleItems = useMemo(
|
|
778
|
+
() => processedItems.slice(0, Math.max(0, resolvedMaxItems)),
|
|
779
|
+
[processedItems, resolvedMaxItems]
|
|
780
|
+
);
|
|
781
|
+
const groups = useMemo(
|
|
782
|
+
() => buildGroups({ items: visibleItems, groupBy, groupByDate, getGroupLabel }),
|
|
783
|
+
[visibleItems, groupBy, groupByDate, getGroupLabel]
|
|
784
|
+
);
|
|
785
|
+
const totalCount = processedItems.length;
|
|
786
|
+
const itemCountLabel = typeof itemCountText === "function" ? itemCountText(visibleItems.length, totalCount) : labels.itemCount(visibleItems.length, totalCount, getRecordLabel(recordLabel, totalCount));
|
|
787
|
+
const canViewMore = visibleItems.length < processedItems.length;
|
|
788
|
+
const shouldShowExternalLoadMore = hasMore && onLoadMore;
|
|
789
|
+
const normalizedTabs = useMemo(
|
|
790
|
+
() => normalizeTabs(tabs, safeItems, tabField, labels),
|
|
791
|
+
[tabs, safeItems, tabField, labels]
|
|
792
|
+
);
|
|
793
|
+
const resolvedShowTabs = showTabs ?? normalizedTabs.length > 1;
|
|
794
|
+
const toolbarNode = renderToolbar ? renderToolbar({
|
|
795
|
+
tab: activeTab,
|
|
796
|
+
tabs: normalizedTabs,
|
|
797
|
+
search: activeSearch,
|
|
798
|
+
filters: activeFilters,
|
|
799
|
+
sort: activeSort,
|
|
800
|
+
totalCount,
|
|
801
|
+
visibleCount: visibleItems.length,
|
|
802
|
+
onTabChange: handleTabChange,
|
|
803
|
+
onSearchChange: handleSearchChange,
|
|
804
|
+
onFilterChange: handleFilterChange,
|
|
805
|
+
onSortChange: handleSortChange
|
|
806
|
+
}) : showToolbar && !loading && !error ? /* @__PURE__ */ React2.createElement(
|
|
807
|
+
FeedToolbar,
|
|
808
|
+
{
|
|
809
|
+
labels,
|
|
810
|
+
search: activeSearch,
|
|
811
|
+
onSearch: handleSearchChange,
|
|
812
|
+
showSearch: resolvedShowSearch,
|
|
813
|
+
searchPlaceholder,
|
|
814
|
+
filters,
|
|
815
|
+
filterValues: activeFilters,
|
|
816
|
+
onFilter: handleFilterChange,
|
|
817
|
+
filterInlineLimit,
|
|
818
|
+
sortOptions,
|
|
819
|
+
sort: activeSort,
|
|
820
|
+
onSort: handleSortChange,
|
|
821
|
+
showItemCount,
|
|
822
|
+
itemCountLabel
|
|
823
|
+
}
|
|
824
|
+
) : null;
|
|
825
|
+
const content = [];
|
|
826
|
+
const collapsibleVisibleItems = visibleItems.filter((item) => itemHasExpandableContent(item, fields));
|
|
827
|
+
const allCollapsed = collapsibleVisibleItems.length > 0 && collapsibleVisibleItems.every((item, idx) => {
|
|
828
|
+
const i = visibleItems.indexOf(item);
|
|
829
|
+
return activeCollapsedIds.includes(getItemKey(item, i >= 0 ? i : idx, getKey));
|
|
830
|
+
});
|
|
831
|
+
const collapseToggle = showCollapseToggle && collapsibleVisibleItems.length > 1 && !loading && !error ? /* @__PURE__ */ React2.createElement(Link, { onClick: allCollapsed ? handleExpandAll : handleCollapseAll }, allCollapsed ? labels.expandAll : labels.collapseAll) : null;
|
|
832
|
+
if (hasValue(title) || hasValue(description) || hasValue(actions) || hasValue(children) || collapseToggle) {
|
|
833
|
+
const headerBody = /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: "xs" }, hasValue(title) && /* @__PURE__ */ React2.createElement(Text, { format: { fontWeight: "demibold" } }, title), hasValue(description) && /* @__PURE__ */ React2.createElement(Text, null, description), children);
|
|
834
|
+
const headerRight = hasValue(actions) || collapseToggle ? /* @__PURE__ */ React2.createElement(Inline, { gap: "sm", align: "center" }, actions, collapseToggle) : null;
|
|
835
|
+
content.push(
|
|
836
|
+
/* @__PURE__ */ React2.createElement(Flex, { key: "header", direction: "row", justify: "between", align: "start", gap: "sm" }, headerBody, headerRight)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const bodyContent = [];
|
|
840
|
+
if (toolbarNode) bodyContent.push(/* @__PURE__ */ React2.createElement(React2.Fragment, { key: "toolbar" }, toolbarNode));
|
|
841
|
+
if (loading) {
|
|
842
|
+
bodyContent.push(
|
|
843
|
+
renderLoadingState ? renderLoadingState({ label: labels.loading }) : (
|
|
844
|
+
// Same EmptyState layout as the empty state (just the "building" image +
|
|
845
|
+
// a loading message) so loading and empty match with no layout shift.
|
|
846
|
+
/* @__PURE__ */ React2.createElement(Tile, { key: "loading" }, /* @__PURE__ */ React2.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React2.createElement(EmptyState, { title: labels.loading, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React2.createElement(Text, null, labels.loadingMessage))))
|
|
847
|
+
)
|
|
848
|
+
);
|
|
849
|
+
} else if (error) {
|
|
850
|
+
bodyContent.push(
|
|
851
|
+
renderErrorState ? renderErrorState({
|
|
852
|
+
error,
|
|
853
|
+
title: typeof error === "string" ? error : labels.errorTitle,
|
|
854
|
+
message: labels.errorMessage
|
|
855
|
+
}) : /* @__PURE__ */ React2.createElement(Alert, { key: "error", variant: "danger", title: typeof error === "string" ? error : labels.errorTitle }, /* @__PURE__ */ React2.createElement(Text, null, labels.errorMessage))
|
|
856
|
+
);
|
|
857
|
+
} else if (processedItems.length === 0) {
|
|
858
|
+
bodyContent.push(
|
|
859
|
+
renderEmptyState ? renderEmptyState({ title: labels.emptyTitle, message: labels.emptyMessage }) : /* @__PURE__ */ React2.createElement(Tile, { key: "empty" }, /* @__PURE__ */ React2.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React2.createElement(EmptyState, { title: labels.emptyTitle, layout: "vertical" }, /* @__PURE__ */ React2.createElement(Text, null, labels.emptyMessage))))
|
|
860
|
+
);
|
|
861
|
+
} else {
|
|
862
|
+
bodyContent.push(
|
|
863
|
+
/* @__PURE__ */ React2.createElement(Flex, { key: "items", direction: "column", gap: compact ? "xs" : gap }, groups.map((group) => /* @__PURE__ */ React2.createElement(Flex, { key: group.key, direction: "column", gap: compact ? "xs" : gap }, hasValue(group.label) && /* @__PURE__ */ React2.createElement(Text, { format: { fontWeight: "demibold" } }, group.label), group.items.map((item, index) => {
|
|
864
|
+
const globalIndex = safeItems.indexOf(item);
|
|
865
|
+
const itemIndex = globalIndex >= 0 ? globalIndex : index;
|
|
866
|
+
const key = getItemKey(item, itemIndex, getKey);
|
|
867
|
+
const node = renderItem ? renderItem(item, itemIndex) : /* @__PURE__ */ React2.createElement(
|
|
868
|
+
DefaultFeedItem,
|
|
869
|
+
{
|
|
870
|
+
item,
|
|
871
|
+
index: itemIndex,
|
|
872
|
+
fields,
|
|
873
|
+
compact,
|
|
874
|
+
avatarSize,
|
|
875
|
+
iconSize,
|
|
876
|
+
collapsible: collapsible !== false && itemHasExpandableContent(item, fields),
|
|
877
|
+
expanded: !activeCollapsedIds.includes(key),
|
|
878
|
+
onToggleExpanded: () => toggleItemExpanded(key),
|
|
879
|
+
renderActor,
|
|
880
|
+
renderTimestamp,
|
|
881
|
+
renderMeta,
|
|
882
|
+
renderActions,
|
|
883
|
+
renderFooter
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
const wrappedNode = itemContainer === "card" || itemContainer === "tile" ? /* @__PURE__ */ React2.createElement(Tile, { compact: true }, node) : node;
|
|
887
|
+
return /* @__PURE__ */ React2.createElement(React2.Fragment, { key }, wrappedNode, showDividers && itemContainer === "none" && index < group.items.length - 1 && /* @__PURE__ */ React2.createElement(Divider, null));
|
|
888
|
+
}))))
|
|
889
|
+
);
|
|
890
|
+
if (canViewMore || shouldShowExternalLoadMore) {
|
|
891
|
+
bodyContent.push(
|
|
892
|
+
/* @__PURE__ */ React2.createElement(Flex, { key: "load-more", direction: "row", justify: "center" }, /* @__PURE__ */ React2.createElement(
|
|
893
|
+
Button,
|
|
894
|
+
{
|
|
895
|
+
onClick: shouldShowExternalLoadMore ? onLoadMore : () => setVisibleCount((count) => count + pageSize),
|
|
896
|
+
disabled: loadingMore,
|
|
897
|
+
variant: "transparent",
|
|
898
|
+
size: "sm"
|
|
899
|
+
},
|
|
900
|
+
loadingMore ? labels.loadingMore : labels.loadMore
|
|
901
|
+
))
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (resolvedShowTabs && !loading && !error) {
|
|
906
|
+
content.push(
|
|
907
|
+
/* @__PURE__ */ React2.createElement(
|
|
908
|
+
Tabs,
|
|
909
|
+
{
|
|
910
|
+
key: "tabs",
|
|
911
|
+
selected: activeTab ?? "all",
|
|
912
|
+
onSelectedChange: handleTabChange,
|
|
913
|
+
variant: tabVariant
|
|
914
|
+
},
|
|
915
|
+
normalizedTabs.map((tab) => /* @__PURE__ */ React2.createElement(Tab, { key: tab.value, tabId: tab.value, title: String(tab.label), disabled: tab.disabled, tooltip: tab.tooltip }, /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: compact ? "xs" : gap }, String(tab.value) === String(activeTab ?? "all") ? bodyContent : null)))
|
|
916
|
+
)
|
|
917
|
+
);
|
|
918
|
+
} else {
|
|
919
|
+
content.push(...bodyContent);
|
|
920
|
+
}
|
|
921
|
+
const feed = /* @__PURE__ */ React2.createElement(Flex, { direction: "column", gap: compact ? "xs" : gap }, content);
|
|
922
|
+
if (container === "card" || container === "tile") return /* @__PURE__ */ React2.createElement(Tile, { compact: true }, feed);
|
|
923
|
+
return feed;
|
|
924
|
+
};
|
|
925
|
+
export {
|
|
926
|
+
Feed
|
|
927
|
+
};
|