qantler-flaz-dataview 1.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.
Files changed (43) hide show
  1. package/package.json +11 -0
  2. package/src/FlazDataView.css +4423 -0
  3. package/src/FlazDataView.jsx +2943 -0
  4. package/src/FlazDataViewList.jsx +787 -0
  5. package/src/MdtActiveFilterBar.jsx +263 -0
  6. package/src/MdtBulkSelectionBar.jsx +91 -0
  7. package/src/MdtColumnPickerPopup.jsx +59 -0
  8. package/src/MdtColumnVisibilityList.jsx +38 -0
  9. package/src/MdtCreateViewModal.jsx +280 -0
  10. package/src/MdtDatetimeValueEditor.jsx +259 -0
  11. package/src/MdtDeleteViewConfirmModal.jsx +83 -0
  12. package/src/MdtDisplayPopup.jsx +170 -0
  13. package/src/MdtEmptyState.jsx +20 -0
  14. package/src/MdtFilterColumnPicker.jsx +66 -0
  15. package/src/MdtFilterOperatorDropdown.jsx +58 -0
  16. package/src/MdtFilterSection.jsx +73 -0
  17. package/src/MdtFilterToolbar.jsx +81 -0
  18. package/src/MdtInlineField.css +221 -0
  19. package/src/MdtInlineField.jsx +187 -0
  20. package/src/MdtInlineFieldRow.jsx +71 -0
  21. package/src/MdtNoDataIllustration.jsx +64 -0
  22. package/src/MdtPopupChecklist.jsx +70 -0
  23. package/src/MdtRowActionsMenu.jsx +281 -0
  24. package/src/MdtSidebarNav.jsx +26 -0
  25. package/src/MdtTabsRow.jsx +354 -0
  26. package/src/MdtTabsSection.jsx +35 -0
  27. package/src/MdtTruncateWithTooltip.jsx +82 -0
  28. package/src/MdtValueEditorPopup.jsx +950 -0
  29. package/src/filterDisplayUtils.jsx +96 -0
  30. package/src/mdtClientSideFilter.js +68 -0
  31. package/src/mdtColumnEditUtils.js +403 -0
  32. package/src/mdtDateFilterUtils.js +135 -0
  33. package/src/mdtExportUtils.js +234 -0
  34. package/src/mdtFilterUtils.js +56 -0
  35. package/src/mdtInlineFieldDisplay.js +66 -0
  36. package/src/mdtMobilePopupPosition.js +107 -0
  37. package/src/mdtMultiSelectDisplay.jsx +279 -0
  38. package/src/mdtSavedViewFilterMatch.js +154 -0
  39. package/src/mdtTabChangeUtils.js +12 -0
  40. package/src/mdtTabStatusRules.js +111 -0
  41. package/src/mdtTableConstants.js +2 -0
  42. package/src/mdtTooltipFormat.js +44 -0
  43. package/src/renderInlineFieldValue.jsx +183 -0
@@ -0,0 +1,787 @@
1
+ import React, { useState, useMemo, useCallback, useEffect } from "react";
2
+ import { ChevronDown, ChevronUp } from "lucide-react";
3
+ import Tooltip from "@mui/material/Tooltip";
4
+ import { resolveTone, STATUS_STYLES } from "../../ui/TableStatusCell";
5
+ import { renderColumnIcon, renderColumnDefIcon } from "../../ui/tableColumnIcons";
6
+ import MdtRowActionsMenu from "./MdtRowActionsMenu";
7
+ import {
8
+ getColumnRowIconName,
9
+ getMultiSelectDisplayLabels,
10
+ renderListColumnChip,
11
+ shouldUseMultiSelectSummary,
12
+ } from "./mdtMultiSelectDisplay";
13
+ import MdtEmptyState from "./MdtEmptyState";
14
+ import { getColumnTooltipText } from "./mdtTooltipFormat";
15
+ import "./FlazDataView.css";
16
+
17
+ const COLUMN_GROUP_ICON_COLOR = "oklch(0.5288 0.0083 230.88)";
18
+ const GROUP_HEAD_ICON_SIZE = 14;
19
+
20
+ const getListSecondaryLabel = (row, _columns, keys, separator = " - ") =>
21
+ keys
22
+ .map((key) => {
23
+ const val = row[key];
24
+ if (val == null || String(val).trim() === "") return null;
25
+ return String(val);
26
+ })
27
+ .filter(Boolean)
28
+ .join(separator);
29
+
30
+ const getTooltipValue = (row, col) => {
31
+ if (shouldUseMultiSelectSummary(col)) {
32
+ const labels = getMultiSelectDisplayLabels(row, col);
33
+ if (labels.length > 1) return "";
34
+ if (labels.length === 1) return labels[0];
35
+ return "None";
36
+ }
37
+ const text = getColumnTooltipText(col, row);
38
+ return text || "None";
39
+ };
40
+
41
+ const getListMetaColumns = (columns, listLeftColumnKeys) => {
42
+ const leftKeySet = new Set(listLeftColumnKeys);
43
+ return columns.filter(
44
+ (col) =>
45
+ col.accessorKey !== "action" &&
46
+ col.header !== "Actions" &&
47
+ !leftKeySet.has(col.accessorKey),
48
+ );
49
+ };
50
+
51
+ /** Skeleton row — same DOM/classes as renderRow so loaders align with real data. */
52
+ const renderListSkeletonRow = (
53
+ key,
54
+ {
55
+ columns,
56
+ listLeftColumnKeys,
57
+ listMainColumnKey,
58
+ listLeftSecondaryKeys,
59
+ showRowSelect,
60
+ showRowMenu = false,
61
+ announce = false,
62
+ },
63
+ ) => {
64
+ const metaCols = getListMetaColumns(columns, listLeftColumnKeys);
65
+ const secondaryKeys =
66
+ listLeftSecondaryKeys != null
67
+ ? listLeftSecondaryKeys
68
+ : listLeftColumnKeys.filter((k) => k !== listMainColumnKey);
69
+ const showSecondary = secondaryKeys.length > 0;
70
+
71
+ return (
72
+ <div
73
+ className={[
74
+ "mdt-group-row",
75
+ "mdt-group-row-skeleton",
76
+ showRowSelect ? "mdt-group-row-with-select" : "",
77
+ ]
78
+ .filter(Boolean)
79
+ .join(" ")}
80
+ key={key}
81
+ role={announce ? "status" : undefined}
82
+ aria-live={announce ? "polite" : undefined}
83
+ aria-hidden={announce ? undefined : true}
84
+ >
85
+ {showRowSelect ? <div className="mdt-cell-select-slot" aria-hidden /> : null}
86
+ <div className="mdt-col-main">
87
+ <div className="mdt-col-main-text">
88
+ <span className="mdt-title">
89
+ <span className="mdt-skeleton-cell mdt-skeleton-main" aria-hidden />
90
+ </span>
91
+ {showSecondary ? (
92
+ <span className="mdt-list-secondary">
93
+ <span
94
+ className="mdt-skeleton-cell mdt-skeleton-secondary"
95
+ aria-hidden
96
+ />
97
+ </span>
98
+ ) : null}
99
+ </div>
100
+ </div>
101
+ <div className="mdt-col-meta-wrap">
102
+ <div className="mdt-col-meta">
103
+ {metaCols.map((col) => (
104
+ <span
105
+ key={col.accessorKey}
106
+ className="mdt-chip mdt-chip-skeleton"
107
+ aria-hidden
108
+ >
109
+ <span className="mdt-skeleton-cell mdt-skeleton-chip" aria-hidden />
110
+ </span>
111
+ ))}
112
+ </div>
113
+ {showRowMenu ? (
114
+ <span className="mdt-list-row-actions" aria-hidden>
115
+ <span className="mdt-skeleton-cell mdt-skeleton-row-menu" aria-hidden />
116
+ </span>
117
+ ) : null}
118
+ </div>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ /**
124
+ * groupByKey: column accessor to group by, or null for a flat list (all rows, no section headers).
125
+ */
126
+ const FlazDataViewList = ({
127
+ wrapRef = null,
128
+ rows,
129
+ columns,
130
+ onRowClick,
131
+ isLoading,
132
+ isLoadingMore = false,
133
+ emptyStateTitle = "No data found",
134
+ skeletonRows = 5,
135
+ onListScroll = null,
136
+ groupByKey = null,
137
+ title = "",
138
+ /** Flat list group header prefix (e.g. Active, Closed, All). Defaults to All. */
139
+ listGroupHeadPrefix = "All",
140
+ /** Flat list group header count; defaults to loaded row count. */
141
+ listGroupHeadCount = null,
142
+ suppressListRowCountFallback = false,
143
+ rowKey = "applicationId",
144
+ /** Primary field on the left (e.g. applicationId). */
145
+ listMainColumnKey = "applicationId",
146
+ /**
147
+ * Column keys pinned to the left (includes main + any shown in the secondary line).
148
+ * Other rendered columns appear as chips on the right.
149
+ */
150
+ listLeftColumnKeys = ["applicationId", "jobOpeningName", "candidateName"],
151
+ /**
152
+ * Keys combined into the secondary line after the main ID (subset of listLeftColumnKeys).
153
+ * Omit or pass [] for no secondary line. Defaults to listLeftColumnKeys minus listMainColumnKey.
154
+ */
155
+ listLeftSecondaryKeys = null,
156
+ /** How secondary values are joined, e.g. " - " → "Job A - Jane Doe". */
157
+ listLeftSecondarySeparator = " - ",
158
+ onRowEdit,
159
+ onRowDownload,
160
+ onRowDelete,
161
+ rowMenuActions = [],
162
+ deleteConfirmConfig = {},
163
+ isRowDeletable = null,
164
+ selectedRowIds = null,
165
+ onToggleRowSelection,
166
+ onToggleGroupSelection,
167
+ hasSelection = false,
168
+ /** Same inline edit as table view — only columns with `editable: true`. */
169
+ onCellActivate = null,
170
+ isColumnEditable = null,
171
+ cellEdit = null,
172
+ cellEditAnchorRef = null,
173
+ }) => {
174
+ const [collapsedGroups, setCollapsedGroups] = useState({});
175
+ const [openRowMenuKey, setOpenRowMenuKey] = useState(null);
176
+ const [openRowMenuRow, setOpenRowMenuRow] = useState(null);
177
+ const [rowMenuPosition, setRowMenuPosition] = useState(null);
178
+
179
+ const showRowMenu = Boolean(
180
+ onRowEdit || onRowDownload || onRowDelete || rowMenuActions.length > 0,
181
+ );
182
+ const showRowSelect = Boolean(selectedRowIds && onToggleRowSelection);
183
+ const flatGroupKey = "__all__";
184
+ const [localSkeletonRows, setLocalSkeletonRows] = useState(skeletonRows);
185
+
186
+ useEffect(() => {
187
+ setLocalSkeletonRows(skeletonRows);
188
+ }, [skeletonRows]);
189
+
190
+ useEffect(() => {
191
+ if (!isLoading) return undefined;
192
+ const update = () => {
193
+ const h = wrapRef?.current?.clientHeight || 0;
194
+ if (!h) return;
195
+ const fit = Math.ceil(Math.max(h - 48, 0) / 48);
196
+ setLocalSkeletonRows(Math.max(skeletonRows, fit));
197
+ };
198
+ update();
199
+ window.addEventListener("resize", update);
200
+ const el = wrapRef?.current;
201
+ let ro;
202
+ if (el) {
203
+ ro = new ResizeObserver(update);
204
+ ro.observe(el);
205
+ }
206
+ return () => {
207
+ window.removeEventListener("resize", update);
208
+ ro?.disconnect();
209
+ };
210
+ }, [isLoading, skeletonRows, wrapRef]);
211
+
212
+ const getRowId = useCallback(
213
+ (row, idx) => {
214
+ const id = row[rowKey] ?? row.id ?? row.applicationId;
215
+ return id != null && id !== "" ? id : idx;
216
+ },
217
+ [rowKey],
218
+ );
219
+
220
+ const closeRowMenu = useCallback(() => {
221
+ setOpenRowMenuKey(null);
222
+ setOpenRowMenuRow(null);
223
+ setRowMenuPosition(null);
224
+ }, []);
225
+
226
+ const handleOpenRowMenu = useCallback((rowId, row, position) => {
227
+ setOpenRowMenuKey(rowId);
228
+ setOpenRowMenuRow(row);
229
+ setRowMenuPosition(position);
230
+ }, []);
231
+
232
+ const groupByColumn = useMemo(
233
+ () => (groupByKey ? columns.find((c) => c.accessorKey === groupByKey) : null),
234
+ [columns, groupByKey],
235
+ );
236
+
237
+ const isStatusGroupColumn =
238
+ groupByKey === "statusName" || groupByColumn?.headerIcon === "status";
239
+
240
+ const renderGroupHeadIcon = (groupLabel, { isAllHeader = false } = {}) => {
241
+ if (isAllHeader) {
242
+ const iconName =
243
+ columns.find((c) => c.accessorKey === "applicationId")?.headerIcon ||
244
+ columns[0]?.headerIcon ||
245
+ "id";
246
+ return (
247
+ <span className="mdt-group-head-icon-wrap">
248
+ {renderColumnIcon(iconName, {
249
+ className: "mdt-group-head-icon",
250
+ size: GROUP_HEAD_ICON_SIZE,
251
+ color: COLUMN_GROUP_ICON_COLOR,
252
+ })}
253
+ </span>
254
+ );
255
+ }
256
+
257
+ if (isStatusGroupColumn) {
258
+ if (typeof groupByColumn?.filterRenderIcon === "function") {
259
+ return (
260
+ <span className="mdt-group-head-icon-wrap">
261
+ {groupByColumn.filterRenderIcon(groupLabel)}
262
+ </span>
263
+ );
264
+ }
265
+ const tone = resolveTone(groupLabel);
266
+ const { Icon, color } = STATUS_STYLES[tone] || STATUS_STYLES.neutral;
267
+ return (
268
+ <Icon
269
+ size={GROUP_HEAD_ICON_SIZE}
270
+ color={color}
271
+ strokeWidth={2.5}
272
+ className="mdt-group-head-icon"
273
+ />
274
+ );
275
+ }
276
+
277
+ const iconName = groupByColumn?.headerIcon || groupByColumn?.rowIcon || "id";
278
+ return (
279
+ <span className="mdt-group-head-icon-wrap">
280
+ {renderColumnIcon(iconName, {
281
+ className: "mdt-group-head-icon",
282
+ size: GROUP_HEAD_ICON_SIZE,
283
+ color: COLUMN_GROUP_ICON_COLOR,
284
+ })}
285
+ </span>
286
+ );
287
+ };
288
+
289
+ const toggleGroup = (title) => {
290
+ setCollapsedGroups((prev) => ({
291
+ ...prev,
292
+ [title]: !prev[title],
293
+ }));
294
+ };
295
+
296
+ const rowCanDelete = useCallback(
297
+ (row) => (typeof isRowDeletable === "function" ? isRowDeletable(row) : true),
298
+ [isRowDeletable],
299
+ );
300
+
301
+ const getGroupRowIds = useCallback(
302
+ (groupRows) =>
303
+ groupRows
304
+ .map((row, idx) => {
305
+ const id = getRowId(row, idx);
306
+ return id != null && id !== "" ? String(id) : null;
307
+ })
308
+ .filter(Boolean),
309
+ [getRowId],
310
+ );
311
+
312
+ const renderGroupHead = ({
313
+ headTitle,
314
+ headTitleSuffix = null,
315
+ count,
316
+ groupKey,
317
+ groupLabel = headTitle,
318
+ isAllHeader = false,
319
+ collapsible = true,
320
+ groupRows = [],
321
+ }) => {
322
+ const groupIds = showRowSelect ? getGroupRowIds(groupRows) : [];
323
+ const allGroupSelected =
324
+ groupIds.length > 0 && groupIds.every((id) => selectedRowIds.has(id));
325
+ const someGroupSelected = groupIds.some((id) => selectedRowIds.has(id));
326
+ const isCollapsed = Boolean(collapsedGroups[groupKey]);
327
+
328
+ return (
329
+ <div
330
+ className={[
331
+ "mdt-group-head",
332
+ showRowSelect ? "mdt-group-head-with-select" : "",
333
+ someGroupSelected ? "mdt-group-head-select-active" : "",
334
+ ]
335
+ .filter(Boolean)
336
+ .join(" ")}
337
+ onClick={collapsible ? () => toggleGroup(groupKey) : undefined}
338
+ style={{
339
+ cursor: collapsible ? "pointer" : "default",
340
+ userSelect: "none",
341
+ justifyContent: "space-between",
342
+ }}
343
+ >
344
+ {showRowSelect && groupIds.length > 0 && (
345
+ <div
346
+ className="mdt-cell-select-slot"
347
+ onClick={(e) => e.stopPropagation()}
348
+ >
349
+ <input
350
+ type="checkbox"
351
+ className="mdt-row-select-checkbox mdt-row-select-checkbox--header"
352
+ checked={allGroupSelected}
353
+ ref={(el) => {
354
+ if (el) {
355
+ el.indeterminate = someGroupSelected && !allGroupSelected;
356
+ }
357
+ }}
358
+ onChange={(e) => onToggleGroupSelection(groupIds, e)}
359
+ onClick={(e) => e.stopPropagation()}
360
+ aria-label={
361
+ isAllHeader
362
+ ? "Select all in list"
363
+ : `Select all in ${headTitle}`
364
+ }
365
+ />
366
+ </div>
367
+ )}
368
+ <div className="mdt-group-head-left">
369
+ {renderGroupHeadIcon(groupLabel, { isAllHeader })}
370
+ <div className="mdt-group-head-labels">
371
+ {headTitleSuffix ? (
372
+ <span className="mdt-group-head-title mdt-group-head-title-split">
373
+ <span className="mdt-group-head-title-prefix">{headTitle}</span>
374
+ <span className="mdt-group-head-title-suffix">
375
+ {headTitleSuffix.toLowerCase()}
376
+ </span>
377
+ </span>
378
+ ) : (
379
+ <span className="mdt-group-head-title">{headTitle}</span>
380
+ )}
381
+ <span className="mdt-group-count">{count}</span>
382
+ </div>
383
+ </div>
384
+ {collapsible && (
385
+ <button
386
+ type="button"
387
+ className="mdt-group-head-toggle"
388
+ aria-expanded={!isCollapsed}
389
+ aria-label={isCollapsed ? "Show group" : "Hide group"}
390
+ title={isCollapsed ? "Show" : "Hide"}
391
+ onClick={(e) => {
392
+ e.stopPropagation();
393
+ toggleGroup(groupKey);
394
+ }}
395
+ >
396
+ {isCollapsed ? (
397
+ <ChevronDown size={16} aria-hidden />
398
+ ) : (
399
+ <ChevronUp size={16} aria-hidden />
400
+ )}
401
+ </button>
402
+ )}
403
+ </div>
404
+ );
405
+ };
406
+
407
+ const getGroupTitle = useCallback((row, key, col) => {
408
+ const raw = row[key];
409
+ if (raw == null) return "Other";
410
+ if (Array.isArray(raw)) {
411
+ if (raw.length === 0) return "Other";
412
+ const labels = getMultiSelectDisplayLabels(row, col);
413
+ if (labels.length > 0) {
414
+ return labels.join(", ");
415
+ }
416
+ return "Other";
417
+ }
418
+ return String(raw).trim() !== "" ? String(raw).trim() : "Other";
419
+ }, []);
420
+
421
+ const groupedRows = useMemo(() => {
422
+ if (!groupByKey) return null;
423
+ const groups = {};
424
+ rows.forEach((row) => {
425
+ const title = getGroupTitle(row, groupByKey, groupByColumn);
426
+ if (!groups[title]) groups[title] = [];
427
+ groups[title].push(row);
428
+ });
429
+ return Object.entries(groups).map(([title, items]) => ({
430
+ title,
431
+ rows: items,
432
+ }));
433
+ }, [rows, groupByKey, groupByColumn, getGroupTitle]);
434
+
435
+ const renderRow = useCallback(
436
+ (row, idx) => {
437
+ const id = getRowId(row, idx);
438
+ const leftKeySet = new Set(listLeftColumnKeys);
439
+ const mainCol =
440
+ columns.find((c) => c.accessorKey === listMainColumnKey) ?? {
441
+ accessorKey: listMainColumnKey,
442
+ header: listMainColumnKey,
443
+ };
444
+ const secondaryKeys =
445
+ listLeftSecondaryKeys != null
446
+ ? listLeftSecondaryKeys
447
+ : listLeftColumnKeys.filter((key) => key !== listMainColumnKey);
448
+ const secondaryLabel = getListSecondaryLabel(
449
+ row,
450
+ columns,
451
+ secondaryKeys,
452
+ listLeftSecondarySeparator,
453
+ );
454
+ const mainCellContent = mainCol?.Cell
455
+ ? mainCol.Cell({ row: { original: row } })
456
+ : row[mainCol?.accessorKey];
457
+ const mainRowIconName = mainCol
458
+ ? getColumnRowIconName(mainCol, {
459
+ hasCustomCell: typeof mainCol.Cell === "function",
460
+ })
461
+ : null;
462
+ const mainRowIcon = mainRowIconName
463
+ ? renderColumnIcon(mainRowIconName, { className: "mdt-row-icon", size: 14 })
464
+ : null;
465
+ const metaCols = columns.filter(
466
+ (col) =>
467
+ col.accessorKey !== "action" &&
468
+ col.header !== "Actions" &&
469
+ !leftKeySet.has(col.accessorKey),
470
+ );
471
+
472
+ const selected = showRowSelect && selectedRowIds.has(String(id));
473
+ const rowDeletable = rowCanDelete(row);
474
+
475
+ return (
476
+ <div
477
+ className={[
478
+ "mdt-group-row",
479
+ showRowSelect ? "mdt-group-row-with-select" : "",
480
+ onRowClick ? "mdt-row-clickable" : "",
481
+ selected ? "mdt-row-selected" : "",
482
+ ]
483
+ .filter(Boolean)
484
+ .join(" ")}
485
+ key={id}
486
+ onClick={onRowClick ? () => onRowClick(id) : undefined}
487
+ >
488
+ {showRowSelect && (
489
+ <div
490
+ className="mdt-cell-select-slot"
491
+ onClick={(e) => e.stopPropagation()}
492
+ >
493
+ <input
494
+ type="checkbox"
495
+ className="mdt-row-select-checkbox"
496
+ checked={selected}
497
+ onChange={(e) => onToggleRowSelection(String(id), e)}
498
+ onClick={(e) => e.stopPropagation()}
499
+ aria-label={`Select row ${id}`}
500
+ />
501
+ </div>
502
+ )}
503
+ <div className="mdt-col-main">
504
+ <div className="mdt-col-main-text">
505
+ {mainCol && (
506
+ <span className="mdt-title">
507
+ {mainRowIcon ? (
508
+ <span className="mdt-cell-with-icon">
509
+ {mainRowIcon}
510
+ <span className="mdt-cell-with-icon-text">{mainCellContent}</span>
511
+ </span>
512
+ ) : (
513
+ mainCellContent
514
+ )}
515
+ </span>
516
+ )}
517
+ {secondaryLabel ? (
518
+ <span className="mdt-list-secondary">{secondaryLabel}</span>
519
+ ) : null}
520
+ </div>
521
+ </div>
522
+ <div className="mdt-col-meta-wrap">
523
+ <div className="mdt-col-meta">
524
+ {metaCols.map((col) => {
525
+ const isMultiSummary = shouldUseMultiSelectSummary(col);
526
+ const multiLabels = isMultiSummary
527
+ ? getMultiSelectDisplayLabels(row, col)
528
+ : [];
529
+ const tooltipText = getTooltipValue(row, col);
530
+ const hasTooltip = !!tooltipText;
531
+ const editable = Boolean(
532
+ onCellActivate && isColumnEditable?.(col),
533
+ );
534
+ const isCellClickable =
535
+ typeof col.onCellClick === "function"
536
+ && (typeof col.isCellClickable !== "function"
537
+ || col.isCellClickable(row));
538
+ const isCellClickDisabled =
539
+ typeof col.onCellClick === "function" && !isCellClickable;
540
+ const isEditingCell =
541
+ cellEdit?.rowId === String(id)
542
+ && cellEdit?.column?.accessorKey === col.accessorKey;
543
+
544
+ const chipContent = renderListColumnChip(row, col);
545
+
546
+ const chipEl = (
547
+ <span
548
+ key={col.accessorKey}
549
+ className={[
550
+ "mdt-chip",
551
+ editable ? "mdt-list-chip-edit-anchor" : "",
552
+ isCellClickable ? "mdt-list-chip-clickable" : "",
553
+ isCellClickDisabled ? "mdt-list-chip-clickable-disabled" : "",
554
+ multiLabels.length > 1 ? "mdt-chip-multi-summary" : "",
555
+ ]
556
+ .filter(Boolean)
557
+ .join(" ")}
558
+ onClick={
559
+ editable
560
+ ? (e) => {
561
+ e.stopPropagation();
562
+ onCellActivate(e, col, row);
563
+ }
564
+ : isCellClickable
565
+ ? (e) => {
566
+ e.stopPropagation();
567
+ col.onCellClick(row, e);
568
+ }
569
+ : undefined
570
+ }
571
+ style={
572
+ editable || isCellClickable
573
+ ? { cursor: "pointer" }
574
+ : isCellClickDisabled
575
+ ? { cursor: "not-allowed" }
576
+ : undefined
577
+ }
578
+ ref={isEditingCell ? cellEditAnchorRef : null}
579
+ >
580
+ {chipContent}
581
+ </span>
582
+ );
583
+
584
+ if (!hasTooltip) return chipEl;
585
+
586
+ return (
587
+ <Tooltip
588
+ key={col.accessorKey}
589
+ title={
590
+ <div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
591
+ <span style={{ fontSize: "11px", fontWeight: "600", color: "#18181b" }}>{col.header}</span>
592
+ <span style={{ fontSize: "11px", color: "#71717a" }}>{tooltipText}</span>
593
+ </div>
594
+ }
595
+ arrow
596
+ placement="top"
597
+ componentsProps={{
598
+ tooltip: {
599
+ sx: {
600
+ bgcolor: '#ffffff',
601
+ color: '#18181b',
602
+ border: '1px solid #e4e4e7',
603
+ borderRadius: 0,
604
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
605
+ padding: '6px 10px',
606
+ '& .MuiTooltip-arrow': {
607
+ color: '#ffffff',
608
+ '&::before': {
609
+ border: '1px solid #e4e4e7',
610
+ backgroundColor: '#ffffff',
611
+ },
612
+ },
613
+ },
614
+ },
615
+ }}
616
+ >
617
+ {chipEl}
618
+ </Tooltip>
619
+ );
620
+ })}
621
+ </div>
622
+ {showRowMenu && (
623
+ <MdtRowActionsMenu
624
+ variant="list"
625
+ row={openRowMenuKey === id ? openRowMenuRow ?? row : row}
626
+ rowId={id}
627
+ isOpen={openRowMenuKey === id}
628
+ menuPosition={openRowMenuKey === id ? rowMenuPosition : null}
629
+ onOpen={handleOpenRowMenu}
630
+ onClose={closeRowMenu}
631
+ onRowEdit={onRowEdit}
632
+ onRowDownload={onRowDownload}
633
+ onRowDelete={onRowDelete}
634
+ rowMenuActions={rowMenuActions}
635
+ deleteConfirmConfig={deleteConfirmConfig}
636
+ isDeletable={rowDeletable}
637
+ alwaysVisible
638
+ />
639
+ )}
640
+ </div>
641
+ </div>
642
+ );
643
+ },
644
+ [
645
+ columns,
646
+ listMainColumnKey,
647
+ listLeftColumnKeys,
648
+ listLeftSecondaryKeys,
649
+ listLeftSecondarySeparator,
650
+ onRowClick,
651
+ getRowId,
652
+ showRowMenu,
653
+ openRowMenuKey,
654
+ openRowMenuRow,
655
+ rowMenuPosition,
656
+ handleOpenRowMenu,
657
+ closeRowMenu,
658
+ onRowEdit,
659
+ onRowDownload,
660
+ onRowDelete,
661
+ rowMenuActions,
662
+ deleteConfirmConfig,
663
+ rowCanDelete,
664
+ showRowSelect,
665
+ selectedRowIds,
666
+ onToggleRowSelection,
667
+ onCellActivate,
668
+ isColumnEditable,
669
+ cellEdit,
670
+ cellEditAnchorRef,
671
+ ],
672
+ );
673
+
674
+ const handleListScroll = (e) => {
675
+ if (openRowMenuKey) closeRowMenu();
676
+ if (onListScroll && e?.currentTarget) {
677
+ const t = e.currentTarget;
678
+ onListScroll({
679
+ scrollTop: t.scrollTop,
680
+ scrollHeight: t.scrollHeight,
681
+ clientHeight: t.clientHeight,
682
+ });
683
+ }
684
+ };
685
+
686
+ const skeletonRowProps = {
687
+ columns,
688
+ listLeftColumnKeys,
689
+ listMainColumnKey,
690
+ listLeftSecondaryKeys,
691
+ showRowSelect,
692
+ showRowMenu,
693
+ };
694
+
695
+ const skeletonRowCount = isLoading ? localSkeletonRows : skeletonRows;
696
+
697
+ const renderSkeletonRows = () =>
698
+ Array.from({ length: skeletonRowCount }).map((_, i) =>
699
+ renderListSkeletonRow(`skeleton-${i}`, skeletonRowProps),
700
+ );
701
+
702
+ const listWrapClass = [
703
+ "mdt-group-wrap",
704
+ hasSelection ? "mdt-has-selection" : "",
705
+ isLoading ? "mdt-group-wrap--loading" : "",
706
+ ]
707
+ .filter(Boolean)
708
+ .join(" ");
709
+
710
+ if (isLoading) {
711
+ return (
712
+ <div
713
+ ref={wrapRef}
714
+ className={listWrapClass}
715
+ onScroll={handleListScroll}
716
+ >
717
+ <div className="mdt-group mdt-group--loading">
718
+ {groupByKey &&
719
+ renderGroupHead({
720
+ headTitle: listGroupHeadPrefix || "All",
721
+ count: listGroupHeadCount ?? 0,
722
+ groupKey: flatGroupKey,
723
+ isAllHeader: true,
724
+ groupRows: [],
725
+ })}
726
+ {renderSkeletonRows()}
727
+ </div>
728
+ </div>
729
+ );
730
+ }
731
+
732
+ if (rows.length === 0) {
733
+ return (
734
+ <div
735
+ ref={wrapRef}
736
+ className={`${listWrapClass} mdt-group-wrap--empty`}
737
+ onScroll={handleListScroll}
738
+ >
739
+ <MdtEmptyState title={emptyStateTitle} />
740
+ </div>
741
+ );
742
+ }
743
+
744
+ /* Flat list — e.g. Display → Group by → None (no grey group header bar) */
745
+ if (!groupByKey) {
746
+ return (
747
+ <div ref={wrapRef} className={listWrapClass} onScroll={handleListScroll}>
748
+ <div className="mdt-group mdt-group--flat">
749
+ {rows.map((row, idx) => renderRow(row, idx))}
750
+ {isLoadingMore &&
751
+ renderListSkeletonRow("loading-more", {
752
+ ...skeletonRowProps,
753
+ announce: true,
754
+ })}
755
+ </div>
756
+ </div>
757
+ );
758
+ }
759
+
760
+ return (
761
+ <div ref={wrapRef} className={listWrapClass} onScroll={handleListScroll}>
762
+ {groupedRows.map((group) => {
763
+ const isCollapsed = collapsedGroups[group.title];
764
+
765
+ return (
766
+ <div className="mdt-group" key={group.title}>
767
+ {renderGroupHead({
768
+ headTitle: group.title,
769
+ count: group.rows.length,
770
+ groupKey: group.title,
771
+ groupLabel: group.title,
772
+ groupRows: group.rows,
773
+ })}
774
+ {!isCollapsed && group.rows.map((row, idx) => renderRow(row, idx))}
775
+ </div>
776
+ );
777
+ })}
778
+ {isLoadingMore &&
779
+ renderListSkeletonRow("loading-more", {
780
+ ...skeletonRowProps,
781
+ announce: true,
782
+ })}
783
+ </div>
784
+ );
785
+ };
786
+
787
+ export default FlazDataViewList;