@altruist03/ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/images/noData.webp +0 -0
- package/dist/index.js +15318 -0
- package/dist/index.js.map +1 -0
- package/dist/noData-XPRHKMFR.webp +0 -0
- package/package.json +51 -4
- package/src/ThemeProvider/ThemeContext.tsx +75 -0
- package/src/ThemeProvider/index.ts +7 -0
- package/src/assets/images/noData.webp +0 -0
- package/src/assets.d.ts +10 -0
- package/src/components/Accordion/Accordion.stories.tsx +64 -0
- package/src/components/Accordion/Accordion.tsx +219 -0
- package/src/components/Accordion/Accordion.types.ts +16 -0
- package/src/components/Avatar/Avatar.stories.tsx +304 -0
- package/src/components/Avatar/Avatar.tsx +211 -0
- package/src/components/Avatar/Avatar.types.ts +33 -0
- package/src/components/Badge/Badge.stories.tsx +143 -0
- package/src/components/Badge/Badge.tsx +138 -0
- package/src/components/Badge/Badge.types.ts +24 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
- package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
- package/src/components/Button/Button.stories.tsx +457 -0
- package/src/components/Button/Button.tsx +202 -0
- package/src/components/Button/Button.types.ts +18 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
- package/src/components/Checkbox/Checkbox.tsx +352 -0
- package/src/components/Checkbox/Checkbox.types.ts +123 -0
- package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
- package/src/components/CloseButton/CloseButton.tsx +117 -0
- package/src/components/CloseButton/CloseButton.types.ts +15 -0
- package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
- package/src/components/DataTable/DataTable.stories.tsx +233 -0
- package/src/components/DataTable/DataTable.tsx +968 -0
- package/src/components/DataTable/DataTable.types.ts +49 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
- package/src/components/DatePicker/DatePicker.tsx +669 -0
- package/src/components/DatePicker/DatePicker.types.ts +105 -0
- package/src/components/DatePicker/DateTimePicker.tsx +35 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
- package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
- package/src/components/Drawer/Drawer.stories.tsx +243 -0
- package/src/components/Drawer/Drawer.tsx +196 -0
- package/src/components/Drawer/Drawer.types.ts +60 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
- package/src/components/FileUpload/FileUpload.tsx +216 -0
- package/src/components/FileUpload/FileUpload.types.ts +80 -0
- package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
- package/src/components/FilterPopover/FilterPopover.tsx +310 -0
- package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
- package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
- package/src/components/ImageViewer/useImagePanRotate.js +102 -0
- package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
- package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
- package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
- package/src/components/Loader/Loader.stories.tsx +95 -0
- package/src/components/Loader/Loader.tsx +132 -0
- package/src/components/Loader/Loader.types.ts +6 -0
- package/src/components/Menu/Menu.tsx +143 -0
- package/src/components/Menu/Menu.types.ts +28 -0
- package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
- package/src/components/MenuButton/MenuButton.tsx +45 -0
- package/src/components/MenuButton/MenuButton.types.ts +21 -0
- package/src/components/Modal/Modal.stories.tsx +266 -0
- package/src/components/Modal/Modal.tsx +399 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Modal/types.ts +126 -0
- package/src/components/Pagination/Pagination.stories.tsx +167 -0
- package/src/components/Pagination/Pagination.tsx +266 -0
- package/src/components/Pagination/Pagination.types.ts +20 -0
- package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
- package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
- package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
- package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
- package/src/components/PhoneField/PhoneField.tsx +284 -0
- package/src/components/PhoneField/PhoneField.types.ts +89 -0
- package/src/components/PhoneField/countryCodes.ts +236 -0
- package/src/components/Progress/Progress.stories.tsx +83 -0
- package/src/components/Progress/Progress.tsx +91 -0
- package/src/components/Progress/Progress.types.ts +22 -0
- package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
- package/src/components/RadioButton/RadioButton.tsx +427 -0
- package/src/components/RadioButton/RadioButton.types.ts +117 -0
- package/src/components/ReportCard/ReportCard.tsx +183 -0
- package/src/components/ReportCard/ReportCard.types.ts +17 -0
- package/src/components/SectionIcon/SectionIcon.tsx +87 -0
- package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
- package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
- package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
- package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
- package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
- package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
- package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
- package/src/components/Selector/Selector.stories.tsx +382 -0
- package/src/components/Selector/Selector.tsx +1310 -0
- package/src/components/Selector/Selector.types.ts +71 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
- package/src/components/Skeleton/Skeleton.tsx +20 -0
- package/src/components/Skeleton/Skeleton.types.ts +3 -0
- package/src/components/Spinner/Spinner.stories.tsx +84 -0
- package/src/components/Spinner/Spinner.tsx +79 -0
- package/src/components/Spinner/Spinner.types.ts +8 -0
- package/src/components/StatCard/StatCard.stories.tsx +125 -0
- package/src/components/StatCard/StatCard.tsx +167 -0
- package/src/components/StatCard/StatCard.types.ts +20 -0
- package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
- package/src/components/StatusBadge/StatusBadge.tsx +74 -0
- package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
- package/src/components/Stepper/Stepper.stories.tsx +85 -0
- package/src/components/Stepper/Stepper.tsx +138 -0
- package/src/components/Stepper/Stepper.types.ts +16 -0
- package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
- package/src/components/SubTabs/SubTabs.tsx +279 -0
- package/src/components/SubTabs/SubTabs.types.ts +44 -0
- package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
- package/src/components/SummaryCard/SummaryCard.tsx +141 -0
- package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
- package/src/components/Switch/Switch.stories.tsx +59 -0
- package/src/components/Switch/Switch.tsx +217 -0
- package/src/components/Switch/Switch.types.ts +22 -0
- package/src/components/Tabs/Tabs.stories.tsx +178 -0
- package/src/components/Tabs/Tabs.tsx +197 -0
- package/src/components/Tabs/Tabs.types.ts +34 -0
- package/src/components/TextField/TextField.stories.tsx +249 -0
- package/src/components/TextField/TextField.tsx +462 -0
- package/src/components/TextField/TextField.types.ts +141 -0
- package/src/components/Toast/Toast.stories.tsx +237 -0
- package/src/components/Toast/Toast.tsx +246 -0
- package/src/components/Toast/Toast.types.ts +31 -0
- package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
- package/src/components/ToggleButton/ToggleButton.tsx +267 -0
- package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
- package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
- package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
- package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
- package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
- package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
- package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
- package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
- package/src/components/index.ts +157 -0
- package/src/icons/ReportDollarIcon.tsx +71 -0
- package/src/icons/index.ts +27 -0
- package/src/index.ts +214 -0
- package/src/storybook/preview.tsx +64 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/types.d.ts +1 -0
- package/src/themes/cpc.ts +4 -0
- package/src/themes/firebird.ts +4 -0
- package/src/tokens/brands/cpc/themes/dark.json +82 -0
- package/src/tokens/brands/cpc/themes/light.json +1 -0
- package/src/tokens/brands/default/brand.json +20 -0
- package/src/tokens/brands/firebird/themes/dark.json +28 -0
- package/src/tokens/brands/firebird/themes/light.json +20 -0
- package/src/tokens/button.ts +24 -0
- package/src/tokens/components/accordion.json +52 -0
- package/src/tokens/components/avatar.json +40 -0
- package/src/tokens/components/badge.json +41 -0
- package/src/tokens/components/breadcrumb.json +13 -0
- package/src/tokens/components/button.json +67 -0
- package/src/tokens/components/card.json +39 -0
- package/src/tokens/components/checkbox.json +64 -0
- package/src/tokens/components/closebutton.json +55 -0
- package/src/tokens/components/datatable.json +40 -0
- package/src/tokens/components/datepicker.json +99 -0
- package/src/tokens/components/drawer.json +23 -0
- package/src/tokens/components/field.json +29 -0
- package/src/tokens/components/fileupload.json +57 -0
- package/src/tokens/components/filterpopover.json +29 -0
- package/src/tokens/components/input.json +31 -0
- package/src/tokens/components/ipwhitelistfield.json +16 -0
- package/src/tokens/components/loader.json +15 -0
- package/src/tokens/components/menu.json +21 -0
- package/src/tokens/components/modal.json +25 -0
- package/src/tokens/components/pagination.json +45 -0
- package/src/tokens/components/phonefield.json +41 -0
- package/src/tokens/components/progress.json +12 -0
- package/src/tokens/components/radio.json +87 -0
- package/src/tokens/components/reportcard.json +17 -0
- package/src/tokens/components/selector.json +87 -0
- package/src/tokens/components/selectscroll.json +65 -0
- package/src/tokens/components/skeleton.json +6 -0
- package/src/tokens/components/stepper.json +38 -0
- package/src/tokens/components/subtabs.json +45 -0
- package/src/tokens/components/switch.json +55 -0
- package/src/tokens/components/tabs.json +38 -0
- package/src/tokens/components/textfield.json +59 -0
- package/src/tokens/components/togglebutton.json +48 -0
- package/src/tokens/components/uploaddropzone.json +61 -0
- package/src/tokens/drawer.ts +37 -0
- package/src/tokens/foundation/breakpoints.json +10 -0
- package/src/tokens/foundation/colors.json +122 -0
- package/src/tokens/foundation/opacity.json +16 -0
- package/src/tokens/foundation/radius.json +14 -0
- package/src/tokens/foundation/size.json +38 -0
- package/src/tokens/foundation/spacing.json +19 -0
- package/src/tokens/foundation/typography.json +43 -0
- package/src/tokens/index.ts +3665 -0
- package/src/tokens/modal.ts +42 -0
- package/src/tokens/semantic/color.json +148 -0
- package/src/tokens/semantic/field.json +28 -0
- package/src/tokens/semantic/opacity.json +5 -0
- package/src/tokens/semantic/size.json +722 -0
- package/src/tokens/semantic/spacing.json +27 -0
- package/src/tokens/semantic/typography.json +87 -0
- package/src/utils/scrollLock.ts +44 -0
- package/README.md +0 -3
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { getDataTableTokensFromResolved } from '../../tokens';
|
|
3
|
+
import { useTokens } from '../../ThemeProvider/ThemeContext';
|
|
4
|
+
import type { DataTableProps } from './DataTable.types';
|
|
5
|
+
import NoDataIm from '../../../assets/images/noData.webp';
|
|
6
|
+
|
|
7
|
+
const SKELETON_KEYFRAMES_ID = 'ds-datatable-skeleton-keyframes';
|
|
8
|
+
const MOBILE_MAX_WIDTH = 767;
|
|
9
|
+
|
|
10
|
+
function useIsMobileScreen() {
|
|
11
|
+
const [isMobile, setIsMobile] = useState(
|
|
12
|
+
() => typeof window !== 'undefined' && window.innerWidth <= MOBILE_MAX_WIDTH
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const mediaQuery = window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH}px)`);
|
|
17
|
+
const update = () => setIsMobile(mediaQuery.matches);
|
|
18
|
+
update();
|
|
19
|
+
mediaQuery.addEventListener('change', update);
|
|
20
|
+
return () => mediaQuery.removeEventListener('change', update);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return isMobile;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const COLUMN_DIVIDER_WIDTH = '1px';
|
|
27
|
+
const EXPANDABLE_TEXT_PREVIEW_LENGTH = 80;
|
|
28
|
+
/** Keeps Read More columns from growing to the full string width under table-layout:auto. */
|
|
29
|
+
const EXPANDABLE_CELL_MAX_WIDTH = 280;
|
|
30
|
+
/** Comfortable min width for month headers like "Jun 2026" when auto-fitting. */
|
|
31
|
+
const DEFAULT_MIN_SCROLL_COLUMN_WIDTH = 104;
|
|
32
|
+
/** Soft cap so ultra-wide screens don't show an excessive number of months at once. */
|
|
33
|
+
const DEFAULT_MAX_SCROLL_COLUMNS = 6;
|
|
34
|
+
|
|
35
|
+
/** Return-from-list row highlight (green, slightly darker for visibility). */
|
|
36
|
+
const HIGHLIGHT_ROW_BACKGROUND = '#C8E6C9';
|
|
37
|
+
const HIGHLIGHT_ROW_BACKGROUND_HOVER = '#A5D6A7';
|
|
38
|
+
const HIGHLIGHT_ROW_TRANSITION = 'background-color 2s ease';
|
|
39
|
+
/** Hover uses a short transition so row feedback stays snappy; highlight fade-out uses HIGHLIGHT_ROW_TRANSITION from React state. */
|
|
40
|
+
const ROW_HOVER_TRANSITION = 'background-color 0.15s ease';
|
|
41
|
+
|
|
42
|
+
/** Align headers/cells from explicit meta, falling back to API columnType. */
|
|
43
|
+
function textAlignForColumnType(column: { meta?: { columnType?: string } } | null | undefined) {
|
|
44
|
+
const raw = String(column?.meta?.columnType ?? '')
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
const numericTypes = new Set([
|
|
48
|
+
'integer',
|
|
49
|
+
'long',
|
|
50
|
+
'int',
|
|
51
|
+
'double',
|
|
52
|
+
'float',
|
|
53
|
+
'number',
|
|
54
|
+
'bigint',
|
|
55
|
+
'decimal',
|
|
56
|
+
]);
|
|
57
|
+
if (numericTypes.has(raw)) return 'right' as const;
|
|
58
|
+
if (raw === 'string') return 'left' as const;
|
|
59
|
+
return 'left' as const;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveExplicitAlign(value: unknown): 'left' | 'right' | 'center' | undefined {
|
|
63
|
+
const raw = String(value ?? '')
|
|
64
|
+
.trim()
|
|
65
|
+
.toLowerCase();
|
|
66
|
+
if (raw === 'left' || raw === 'right' || raw === 'center') return raw;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function headerAlignForColumn(column: { meta?: { headerAlign?: string } } | null | undefined) {
|
|
71
|
+
return resolveExplicitAlign(column?.meta?.headerAlign) ?? ('center' as const);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cellAlignForColumn(
|
|
75
|
+
column: { meta?: { cellAlign?: string; columnType?: string } } | null | undefined
|
|
76
|
+
) {
|
|
77
|
+
return resolveExplicitAlign(column?.meta?.cellAlign) ?? textAlignForColumnType(column);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stickyPositionForColumn(
|
|
81
|
+
column: { meta?: { sticky?: string } } | null | undefined
|
|
82
|
+
): 'left' | 'right' | undefined {
|
|
83
|
+
const raw = String(column?.meta?.sticky ?? '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
if (raw === 'left' || raw === 'right') return raw;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extraCellStyleFromColumn(cell: any): React.CSSProperties {
|
|
91
|
+
const getCellStyle = cell?.column?.meta?.getCellStyle;
|
|
92
|
+
if (typeof getCellStyle !== 'function') return {};
|
|
93
|
+
const style = getCellStyle(cell);
|
|
94
|
+
return style && typeof style === 'object' ? (style as React.CSSProperties) : {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function columnMinWidthStyle(column: any): React.CSSProperties {
|
|
98
|
+
const minWidth = column?.meta?.minWidth;
|
|
99
|
+
if (minWidth == null || minWidth === '') return {};
|
|
100
|
+
return { minWidth };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function columnHeaderExtraStyle(column: any): React.CSSProperties {
|
|
104
|
+
if (!column?.meta?.nowrapHeader) return {};
|
|
105
|
+
return { whiteSpace: 'nowrap' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Match highlighted row when API uses userId, clientId, clientMasterId, positionId, deviceId, or id
|
|
110
|
+
* on row.original. positionId is checked before deviceId because GPS position rows carry both
|
|
111
|
+
* (deviceId there is the device's IMEI, not a row identifier) and positionId is the one that's
|
|
112
|
+
* actually unique per row.
|
|
113
|
+
*/
|
|
114
|
+
function getRowClientIdForHighlight(original: unknown): string {
|
|
115
|
+
if (!original || typeof original !== 'object') return '';
|
|
116
|
+
const o = original as Record<string, unknown>;
|
|
117
|
+
const v =
|
|
118
|
+
o.selectionKey ??
|
|
119
|
+
o.otherDeductionTransactionId ??
|
|
120
|
+
o.loanId ??
|
|
121
|
+
o.userId ??
|
|
122
|
+
o.clientId ??
|
|
123
|
+
o.clientMasterId ??
|
|
124
|
+
o.positionId ??
|
|
125
|
+
o.deviceId ??
|
|
126
|
+
o.id;
|
|
127
|
+
return v != null ? String(v) : '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ensureSkeletonKeyframes() {
|
|
131
|
+
if (typeof document === 'undefined') return;
|
|
132
|
+
if (document.getElementById(SKELETON_KEYFRAMES_ID)) return;
|
|
133
|
+
|
|
134
|
+
const css = `
|
|
135
|
+
@keyframes dsSkeletonShimmer {
|
|
136
|
+
0% { background-position: -200px 0; }
|
|
137
|
+
100% { background-position: calc(200px + 100%) 0; }
|
|
138
|
+
}`;
|
|
139
|
+
|
|
140
|
+
const style = document.createElement('style');
|
|
141
|
+
style.id = SKELETON_KEYFRAMES_ID;
|
|
142
|
+
style.textContent = css;
|
|
143
|
+
document.head.appendChild(style);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Sort icon – uses the project's sorting_icon.svg asset */
|
|
147
|
+
// import ArrowUpIcon from "@/assets/images/sorting_icon.svg";
|
|
148
|
+
|
|
149
|
+
function isHtmlColumn(column: { meta?: { columnType?: string } } | null | undefined) {
|
|
150
|
+
return String(column?.meta?.columnType ?? '').toLowerCase() === 'html';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isExpandableTextValue(
|
|
154
|
+
value: unknown,
|
|
155
|
+
column?: { meta?: { columnType?: string; disableExpandableCell?: boolean } } | null
|
|
156
|
+
) {
|
|
157
|
+
if (isHtmlColumn(column) || column?.meta?.disableExpandableCell) return false;
|
|
158
|
+
if (typeof value !== 'string') return false;
|
|
159
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(value)) return false;
|
|
160
|
+
return value.length > EXPANDABLE_TEXT_PREVIEW_LENGTH;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Status / badge columns must stay on one line; squeeze → horizontal scroll instead of a broken badge. */
|
|
164
|
+
function isStatusBadgeColumn(
|
|
165
|
+
column:
|
|
166
|
+
| {
|
|
167
|
+
id?: string;
|
|
168
|
+
Header?: unknown;
|
|
169
|
+
accessor?: unknown;
|
|
170
|
+
meta?: { columnType?: string; wrapText?: boolean };
|
|
171
|
+
}
|
|
172
|
+
| null
|
|
173
|
+
| undefined
|
|
174
|
+
) {
|
|
175
|
+
if (isHtmlColumn(column)) return true;
|
|
176
|
+
const key = String(column?.id ?? column?.accessor ?? '')
|
|
177
|
+
.trim()
|
|
178
|
+
.toLowerCase();
|
|
179
|
+
const header = String(column?.Header ?? '')
|
|
180
|
+
.trim()
|
|
181
|
+
.toLowerCase();
|
|
182
|
+
return key === 'status' || key === 'statusname' || key === 'status_name' || header === 'status';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ExpandableCell = ({
|
|
186
|
+
value,
|
|
187
|
+
renderedCell,
|
|
188
|
+
column,
|
|
189
|
+
}: {
|
|
190
|
+
value: any;
|
|
191
|
+
renderedCell: React.ReactNode;
|
|
192
|
+
column?: { meta?: { columnType?: string; disableExpandableCell?: boolean } } | null;
|
|
193
|
+
}) => {
|
|
194
|
+
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
195
|
+
|
|
196
|
+
if (!isExpandableTextValue(value, column)) {
|
|
197
|
+
return <>{renderedCell}</>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div
|
|
202
|
+
style={{
|
|
203
|
+
display: 'block',
|
|
204
|
+
boxSizing: 'border-box',
|
|
205
|
+
width: '100%',
|
|
206
|
+
maxWidth: EXPANDABLE_CELL_MAX_WIDTH,
|
|
207
|
+
whiteSpace: 'normal',
|
|
208
|
+
overflowWrap: 'anywhere',
|
|
209
|
+
wordBreak: 'break-word',
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{isExpanded ?
|
|
213
|
+
renderedCell
|
|
214
|
+
: <span>{`${value.slice(0, EXPANDABLE_TEXT_PREVIEW_LENGTH).trimEnd()}...`}</span>}
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
setIsExpanded(!isExpanded);
|
|
220
|
+
}}
|
|
221
|
+
style={{
|
|
222
|
+
color: '#2563eb',
|
|
223
|
+
background: 'none',
|
|
224
|
+
border: 'none',
|
|
225
|
+
cursor: 'pointer',
|
|
226
|
+
padding: 0,
|
|
227
|
+
marginLeft: 2,
|
|
228
|
+
fontSize: '0.85em',
|
|
229
|
+
textDecoration: 'underline',
|
|
230
|
+
fontWeight: 500,
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{isExpanded ? 'Read Less' : 'Read More'}
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
function shouldWrapTableCell(
|
|
240
|
+
column:
|
|
241
|
+
| {
|
|
242
|
+
id?: string;
|
|
243
|
+
Header?: unknown;
|
|
244
|
+
accessor?: unknown;
|
|
245
|
+
meta?: { wrapText?: boolean; columnType?: string };
|
|
246
|
+
}
|
|
247
|
+
| null
|
|
248
|
+
| undefined,
|
|
249
|
+
cellValue: unknown,
|
|
250
|
+
preventHorizontalScroll: boolean
|
|
251
|
+
) {
|
|
252
|
+
// Status badges / HTML pills must never wrap mid-word; prefer horizontal scroll.
|
|
253
|
+
if (isStatusBadgeColumn(column) && !column?.meta?.wrapText) return false;
|
|
254
|
+
if (preventHorizontalScroll) return true;
|
|
255
|
+
if (column?.meta?.wrapText) return true;
|
|
256
|
+
return (
|
|
257
|
+
typeof cellValue === 'string' &&
|
|
258
|
+
cellValue.length > EXPANDABLE_TEXT_PREVIEW_LENGTH &&
|
|
259
|
+
!/^\d{4}-\d{2}-\d{2}T/.test(cellValue)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function DataTable({
|
|
264
|
+
getTableProps,
|
|
265
|
+
getTableBodyProps,
|
|
266
|
+
headerGroups,
|
|
267
|
+
page,
|
|
268
|
+
rows,
|
|
269
|
+
prepareRow,
|
|
270
|
+
loading = false,
|
|
271
|
+
Status,
|
|
272
|
+
length,
|
|
273
|
+
emptyStateHeight = 300,
|
|
274
|
+
stickyHeader = true,
|
|
275
|
+
className,
|
|
276
|
+
highlightRowClientId = null,
|
|
277
|
+
getHighlightRowId,
|
|
278
|
+
preventHorizontalScroll = false,
|
|
279
|
+
frozenPaneMaxWidth = '42%',
|
|
280
|
+
visibleScrollColumns,
|
|
281
|
+
minScrollColumnWidth = DEFAULT_MIN_SCROLL_COLUMN_WIDTH,
|
|
282
|
+
maxScrollColumns = DEFAULT_MAX_SCROLL_COLUMNS,
|
|
283
|
+
}: DataTableProps) {
|
|
284
|
+
const resolvedTokens = useTokens();
|
|
285
|
+
const isMobileScreen = useIsMobileScreen();
|
|
286
|
+
const tokens = getDataTableTokensFromResolved(resolvedTokens);
|
|
287
|
+
const injectedRef = useRef(false);
|
|
288
|
+
const frozenTableRef = useRef<HTMLTableElement>(null);
|
|
289
|
+
const scrollTableRef = useRef<HTMLTableElement>(null);
|
|
290
|
+
const frozenPaneRef = useRef<HTMLDivElement>(null);
|
|
291
|
+
const scrollPaneRef = useRef<HTMLDivElement>(null);
|
|
292
|
+
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null);
|
|
293
|
+
const tableBorderRadius = resolvedTokens.foundation?.radius?.lg ?? '8px';
|
|
294
|
+
const bodyColumnDividerColor =
|
|
295
|
+
resolvedTokens.semantic?.color?.secondary?.default ?? tokens.cellBorderBottom;
|
|
296
|
+
|
|
297
|
+
// function SortIcon({ direction }: { direction: 'asc' | 'desc' | null; color?: string }) {
|
|
298
|
+
// return (
|
|
299
|
+
// <img
|
|
300
|
+
// // src={ArrowUpIcon}
|
|
301
|
+
// src={''}
|
|
302
|
+
// alt={
|
|
303
|
+
// direction === 'asc' ? 'Sort ascending'
|
|
304
|
+
// : direction === 'desc' ?
|
|
305
|
+
// 'Sort descending'
|
|
306
|
+
// : 'Sortable'
|
|
307
|
+
// }
|
|
308
|
+
// style={{
|
|
309
|
+
// marginLeft: 8,
|
|
310
|
+
// width: tokens.sortIconWidth,
|
|
311
|
+
// height: tokens.sortIconHeight,
|
|
312
|
+
// verticalAlign: 'middle',
|
|
313
|
+
// opacity: direction ? 1 : 0.4,
|
|
314
|
+
// transform: direction === 'desc' ? 'rotate(180deg)' : 'none',
|
|
315
|
+
// transition: 'transform 0.2s, opacity 0.2s',
|
|
316
|
+
// }}
|
|
317
|
+
// />
|
|
318
|
+
// );
|
|
319
|
+
// }
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (!injectedRef.current) {
|
|
323
|
+
ensureSkeletonKeyframes();
|
|
324
|
+
injectedRef.current = true;
|
|
325
|
+
}
|
|
326
|
+
}, []);
|
|
327
|
+
|
|
328
|
+
const skelCols = headerGroups?.length > 0 ? headerGroups[0].headers.length : 3;
|
|
329
|
+
const skeletonRows = Status?.pageSize || length || 4;
|
|
330
|
+
const renderRows = page || rows || [];
|
|
331
|
+
const hasRows = renderRows.length > 0;
|
|
332
|
+
const allHeaders = headerGroups?.[0]?.headers ?? [];
|
|
333
|
+
const frozenHeaders = allHeaders.filter(
|
|
334
|
+
(header: any) => stickyPositionForColumn(header) === 'left'
|
|
335
|
+
);
|
|
336
|
+
const scrollHeaders = allHeaders.filter(
|
|
337
|
+
(header: any) => stickyPositionForColumn(header) !== 'left'
|
|
338
|
+
);
|
|
339
|
+
const hasFrozenSplit =
|
|
340
|
+
!isMobileScreen &&
|
|
341
|
+
!preventHorizontalScroll &&
|
|
342
|
+
frozenHeaders.length > 0 &&
|
|
343
|
+
scrollHeaders.length > 0 &&
|
|
344
|
+
(loading || hasRows);
|
|
345
|
+
const useEqualWidthScroll =
|
|
346
|
+
visibleScrollColumns === 'auto' ||
|
|
347
|
+
(typeof visibleScrollColumns === 'number' && visibleScrollColumns > 0);
|
|
348
|
+
const resolvedMinScrollColumnWidth =
|
|
349
|
+
typeof minScrollColumnWidth === 'number' && minScrollColumnWidth > 0 ?
|
|
350
|
+
minScrollColumnWidth
|
|
351
|
+
: DEFAULT_MIN_SCROLL_COLUMN_WIDTH;
|
|
352
|
+
const resolvedMaxScrollColumns =
|
|
353
|
+
typeof maxScrollColumns === 'number' && maxScrollColumns > 0 ?
|
|
354
|
+
maxScrollColumns
|
|
355
|
+
: DEFAULT_MAX_SCROLL_COLUMNS;
|
|
356
|
+
|
|
357
|
+
const resolveColumnsInView = (paneWidth: number, columnCount: number) => {
|
|
358
|
+
if (!useEqualWidthScroll || columnCount === 0 || paneWidth <= 0) return null;
|
|
359
|
+
if (visibleScrollColumns === 'auto') {
|
|
360
|
+
const fitted = Math.max(1, Math.floor(paneWidth / resolvedMinScrollColumnWidth));
|
|
361
|
+
return Math.min(fitted, resolvedMaxScrollColumns, columnCount);
|
|
362
|
+
}
|
|
363
|
+
return Math.min(visibleScrollColumns as number, columnCount);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (!hasFrozenSplit) return;
|
|
368
|
+
const leftTable = frozenTableRef.current;
|
|
369
|
+
const rightTable = scrollTableRef.current;
|
|
370
|
+
const leftPane = frozenPaneRef.current;
|
|
371
|
+
const rightPane = scrollPaneRef.current;
|
|
372
|
+
if (!leftTable || !rightTable) return;
|
|
373
|
+
|
|
374
|
+
let rafId = 0;
|
|
375
|
+
let running = false;
|
|
376
|
+
|
|
377
|
+
const applyEqualScrollColumnWidths = () => {
|
|
378
|
+
if (!useEqualWidthScroll || !rightPane) return;
|
|
379
|
+
const columnCount = scrollHeaders.length;
|
|
380
|
+
if (columnCount === 0) return;
|
|
381
|
+
|
|
382
|
+
const paneWidth = rightPane.clientWidth;
|
|
383
|
+
if (paneWidth <= 0) return;
|
|
384
|
+
|
|
385
|
+
const columnsInView = resolveColumnsInView(paneWidth, columnCount);
|
|
386
|
+
if (!columnsInView) return;
|
|
387
|
+
|
|
388
|
+
const colWidth = paneWidth / columnsInView;
|
|
389
|
+
const totalWidth = colWidth * columnCount;
|
|
390
|
+
|
|
391
|
+
rightTable.style.tableLayout = 'fixed';
|
|
392
|
+
rightTable.style.width = `${totalWidth}px`;
|
|
393
|
+
rightTable.style.minWidth = `${totalWidth}px`;
|
|
394
|
+
|
|
395
|
+
rightTable.querySelectorAll('thead th').forEach((th) => {
|
|
396
|
+
const el = th as HTMLElement;
|
|
397
|
+
el.style.width = `${colWidth}px`;
|
|
398
|
+
el.style.minWidth = `${colWidth}px`;
|
|
399
|
+
el.style.maxWidth = `${colWidth}px`;
|
|
400
|
+
el.style.boxSizing = 'border-box';
|
|
401
|
+
el.style.overflow = 'hidden';
|
|
402
|
+
});
|
|
403
|
+
rightTable.querySelectorAll('tbody td').forEach((td) => {
|
|
404
|
+
const el = td as HTMLElement;
|
|
405
|
+
el.style.width = `${colWidth}px`;
|
|
406
|
+
el.style.minWidth = `${colWidth}px`;
|
|
407
|
+
el.style.maxWidth = `${colWidth}px`;
|
|
408
|
+
el.style.boxSizing = 'border-box';
|
|
409
|
+
el.style.overflow = 'hidden';
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const sync = () => {
|
|
414
|
+
if (running) return;
|
|
415
|
+
running = true;
|
|
416
|
+
rafId = requestAnimationFrame(() => {
|
|
417
|
+
try {
|
|
418
|
+
applyEqualScrollColumnWidths();
|
|
419
|
+
|
|
420
|
+
const leftRows = leftTable.querySelectorAll('tr');
|
|
421
|
+
const rightRows = rightTable.querySelectorAll('tr');
|
|
422
|
+
const count = Math.min(leftRows.length, rightRows.length);
|
|
423
|
+
for (let i = 0; i < count; i += 1) {
|
|
424
|
+
const leftRow = leftRows[i] as HTMLElement;
|
|
425
|
+
const rightRow = rightRows[i] as HTMLElement;
|
|
426
|
+
leftRow.style.height = 'auto';
|
|
427
|
+
rightRow.style.height = 'auto';
|
|
428
|
+
const height = Math.max(
|
|
429
|
+
leftRow.getBoundingClientRect().height,
|
|
430
|
+
rightRow.getBoundingClientRect().height
|
|
431
|
+
);
|
|
432
|
+
leftRow.style.height = `${height}px`;
|
|
433
|
+
rightRow.style.height = `${height}px`;
|
|
434
|
+
}
|
|
435
|
+
if (leftPane && rightPane) {
|
|
436
|
+
const scrollbarHeight = Math.max(0, rightPane.offsetHeight - rightPane.clientHeight);
|
|
437
|
+
leftPane.style.paddingBottom = `${scrollbarHeight}px`;
|
|
438
|
+
}
|
|
439
|
+
} finally {
|
|
440
|
+
running = false;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
sync();
|
|
446
|
+
window.addEventListener('resize', sync);
|
|
447
|
+
|
|
448
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
449
|
+
if (typeof ResizeObserver !== 'undefined' && rightPane) {
|
|
450
|
+
resizeObserver = new ResizeObserver(sync);
|
|
451
|
+
resizeObserver.observe(rightPane);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return () => {
|
|
455
|
+
running = false;
|
|
456
|
+
cancelAnimationFrame(rafId);
|
|
457
|
+
window.removeEventListener('resize', sync);
|
|
458
|
+
resizeObserver?.disconnect();
|
|
459
|
+
};
|
|
460
|
+
}, [
|
|
461
|
+
hasFrozenSplit,
|
|
462
|
+
loading,
|
|
463
|
+
renderRows.length,
|
|
464
|
+
allHeaders.length,
|
|
465
|
+
scrollHeaders.length,
|
|
466
|
+
useEqualWidthScroll,
|
|
467
|
+
visibleScrollColumns,
|
|
468
|
+
resolvedMinScrollColumnWidth,
|
|
469
|
+
resolvedMaxScrollColumns,
|
|
470
|
+
highlightRowClientId,
|
|
471
|
+
getHighlightRowId,
|
|
472
|
+
]);
|
|
473
|
+
|
|
474
|
+
/* -------- Styles -------- */
|
|
475
|
+
const tableStyle: React.CSSProperties = {
|
|
476
|
+
width: '100%',
|
|
477
|
+
minWidth: '100%',
|
|
478
|
+
borderCollapse: 'collapse',
|
|
479
|
+
tableLayout: preventHorizontalScroll ? 'fixed' : 'auto',
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const containerStyle: React.CSSProperties = {
|
|
483
|
+
width: '100%',
|
|
484
|
+
overflowX: preventHorizontalScroll ? 'hidden' : 'auto',
|
|
485
|
+
overflowY: 'auto',
|
|
486
|
+
border: `1px solid ${tokens.tableBorder}`,
|
|
487
|
+
borderRadius: tableBorderRadius,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const thStyle = (
|
|
491
|
+
align: string = 'center',
|
|
492
|
+
isLastColumn: boolean = false
|
|
493
|
+
): React.CSSProperties => ({
|
|
494
|
+
padding: tokens.headerPadding,
|
|
495
|
+
fontSize: tokens.headerFontSize,
|
|
496
|
+
fontWeight: Number(tokens.headerFontWeight) || 600,
|
|
497
|
+
color: tokens.headerColor,
|
|
498
|
+
backgroundColor: tokens.headerBackground,
|
|
499
|
+
borderBottom: `1px solid ${tokens.headerBorderBottom}`,
|
|
500
|
+
borderRight: isLastColumn ? 'none' : `1px solid ${tokens.headerBorderRight}`,
|
|
501
|
+
textAlign: align as any,
|
|
502
|
+
whiteSpace: 'pre-line',
|
|
503
|
+
overflowWrap: 'break-word',
|
|
504
|
+
verticalAlign: 'middle',
|
|
505
|
+
cursor: 'default',
|
|
506
|
+
position: stickyHeader ? 'sticky' : 'relative',
|
|
507
|
+
top: stickyHeader ? 0 : undefined,
|
|
508
|
+
zIndex: stickyHeader ? 1 : undefined,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const tdStyle = (
|
|
512
|
+
isLastRow: boolean = false,
|
|
513
|
+
textAlign: 'left' | 'right' | 'center' = 'center',
|
|
514
|
+
isLastColumn: boolean = false
|
|
515
|
+
): React.CSSProperties => ({
|
|
516
|
+
padding: tokens.cellPadding,
|
|
517
|
+
fontSize: tokens.cellFontSize,
|
|
518
|
+
color: tokens.cellColor,
|
|
519
|
+
borderBottom: isLastRow ? 'none' : `1px solid ${tokens.cellBorderBottom}`,
|
|
520
|
+
borderRight: isLastColumn ? 'none' : `${COLUMN_DIVIDER_WIDTH} solid ${bodyColumnDividerColor}`,
|
|
521
|
+
textAlign,
|
|
522
|
+
textTransform: 'none',
|
|
523
|
+
verticalAlign: 'middle',
|
|
524
|
+
whiteSpace: 'nowrap',
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const stickyStyle = (
|
|
528
|
+
stickyPosition: 'left' | 'right' | undefined,
|
|
529
|
+
isHeader: boolean
|
|
530
|
+
): React.CSSProperties => {
|
|
531
|
+
if (!stickyPosition || isMobileScreen) return {};
|
|
532
|
+
return {
|
|
533
|
+
position: 'sticky',
|
|
534
|
+
[stickyPosition]: 0,
|
|
535
|
+
backgroundColor: isHeader ? tokens.headerBackground : 'inherit',
|
|
536
|
+
zIndex: isHeader ? 3 : 2,
|
|
537
|
+
boxShadow:
|
|
538
|
+
stickyPosition === 'right' ?
|
|
539
|
+
'-1px 0 0 rgba(229, 231, 235, 0.9)'
|
|
540
|
+
: '1px 0 0 rgba(229, 231, 235, 0.9)',
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const skeletonBarStyle: React.CSSProperties = {
|
|
545
|
+
height: tokens.skeletonHeight,
|
|
546
|
+
borderRadius: tokens.skeletonRadius,
|
|
547
|
+
background: `linear-gradient(90deg, ${tokens.skeletonBaseColor} 25%, transparent 50%, ${tokens.skeletonBaseColor} 75%)`,
|
|
548
|
+
backgroundSize: '400px 100%',
|
|
549
|
+
animation: 'dsSkeletonShimmer 1.5s ease-in-out infinite',
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const splitContainerStyle: React.CSSProperties = {
|
|
553
|
+
width: '100%',
|
|
554
|
+
display: 'flex',
|
|
555
|
+
alignItems: 'stretch',
|
|
556
|
+
overflow: 'hidden',
|
|
557
|
+
border: `1px solid ${tokens.tableBorder}`,
|
|
558
|
+
borderRadius: tableBorderRadius,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const frozenPaneStyle: React.CSSProperties = {
|
|
562
|
+
// Hug content so the right pane starts flush against the last frozen column.
|
|
563
|
+
// Do not shrink or scroll horizontally — frozen columns stay fully visible.
|
|
564
|
+
flex: '0 0 auto',
|
|
565
|
+
width: 'max-content',
|
|
566
|
+
maxWidth: frozenPaneMaxWidth,
|
|
567
|
+
overflowX: 'hidden',
|
|
568
|
+
overflowY: 'hidden',
|
|
569
|
+
boxShadow: '8px 0 14px -8px rgba(15, 23, 42, 0.28)',
|
|
570
|
+
zIndex: 2,
|
|
571
|
+
backgroundColor: tokens.rowBackground,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const scrollPaneStyle: React.CSSProperties = {
|
|
575
|
+
flex: '1 1 0%',
|
|
576
|
+
minWidth: 0,
|
|
577
|
+
overflowX: 'auto',
|
|
578
|
+
overflowY: 'hidden',
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const frozenTableStyle: React.CSSProperties = {
|
|
582
|
+
borderCollapse: 'collapse',
|
|
583
|
+
tableLayout: 'auto',
|
|
584
|
+
width: 'max-content',
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const scrollTableStyle: React.CSSProperties = {
|
|
588
|
+
borderCollapse: 'collapse',
|
|
589
|
+
tableLayout: useEqualWidthScroll ? 'fixed' : 'auto',
|
|
590
|
+
width: useEqualWidthScroll ? undefined : 'max-content',
|
|
591
|
+
minWidth: useEqualWidthScroll ? undefined : '100%',
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const renderHeaderRow = (
|
|
595
|
+
headers: any[],
|
|
596
|
+
{
|
|
597
|
+
headerLoading = false,
|
|
598
|
+
disableSticky = false,
|
|
599
|
+
keepLastDivider = false,
|
|
600
|
+
}: { headerLoading?: boolean; disableSticky?: boolean; keepLastDivider?: boolean } = {}
|
|
601
|
+
) => (
|
|
602
|
+
<tr>
|
|
603
|
+
{headerLoading ?
|
|
604
|
+
headers.map((col: any, i: number) => (
|
|
605
|
+
<th
|
|
606
|
+
key={`skel-h-${col?.id ?? i}`}
|
|
607
|
+
style={{
|
|
608
|
+
...thStyle(
|
|
609
|
+
headerAlignForColumn(col),
|
|
610
|
+
keepLastDivider ? false : i === headers.length - 1
|
|
611
|
+
),
|
|
612
|
+
...columnMinWidthStyle(col),
|
|
613
|
+
}}
|
|
614
|
+
>
|
|
615
|
+
<div style={skeletonBarStyle} />
|
|
616
|
+
</th>
|
|
617
|
+
))
|
|
618
|
+
: headers.map((col: any, colIdx: number) => {
|
|
619
|
+
const isLast = keepLastDivider ? false : colIdx === headers.length - 1;
|
|
620
|
+
const headerAlign = headerAlignForColumn(col);
|
|
621
|
+
const stickyPosition = disableSticky ? undefined : stickyPositionForColumn(col);
|
|
622
|
+
const headerProps = col.getHeaderProps(
|
|
623
|
+
col.getSortByToggleProps ? col.getSortByToggleProps() : undefined
|
|
624
|
+
);
|
|
625
|
+
return (
|
|
626
|
+
<th
|
|
627
|
+
{...headerProps}
|
|
628
|
+
style={{
|
|
629
|
+
...thStyle(headerAlign, isLast),
|
|
630
|
+
...stickyStyle(stickyPosition, true),
|
|
631
|
+
...columnMinWidthStyle(col),
|
|
632
|
+
...columnHeaderExtraStyle(col),
|
|
633
|
+
...(col.canSort ? { cursor: 'pointer', userSelect: 'none' } : {}),
|
|
634
|
+
}}
|
|
635
|
+
>
|
|
636
|
+
{col.render('Header')}
|
|
637
|
+
</th>
|
|
638
|
+
);
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
</tr>
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
const renderBodyCell = (
|
|
645
|
+
cell: any,
|
|
646
|
+
rowIdx: number,
|
|
647
|
+
cellIdx: number,
|
|
648
|
+
cellCount: number,
|
|
649
|
+
{
|
|
650
|
+
disableSticky = false,
|
|
651
|
+
totalRows = renderRows.length,
|
|
652
|
+
keepLastDivider = false,
|
|
653
|
+
}: { disableSticky?: boolean; totalRows?: number; keepLastDivider?: boolean } = {}
|
|
654
|
+
) => {
|
|
655
|
+
const align = cellAlignForColumn(cell.column);
|
|
656
|
+
const stickyPosition = disableSticky ? undefined : stickyPositionForColumn(cell.column);
|
|
657
|
+
const isStatusBadge = isStatusBadgeColumn(cell.column);
|
|
658
|
+
const shouldWrap = shouldWrapTableCell(cell.column, cell.value, preventHorizontalScroll);
|
|
659
|
+
const isExpandableText = isExpandableTextValue(cell.value, cell.column);
|
|
660
|
+
const cellProps = cell.getCellProps();
|
|
661
|
+
const cellClassName = [cellProps.className, isStatusBadge ? 'ds-datatable-cell--status' : '']
|
|
662
|
+
.filter(Boolean)
|
|
663
|
+
.join(' ');
|
|
664
|
+
return (
|
|
665
|
+
<td
|
|
666
|
+
{...cellProps}
|
|
667
|
+
className={cellClassName || undefined}
|
|
668
|
+
style={{
|
|
669
|
+
...tdStyle(
|
|
670
|
+
rowIdx === totalRows - 1,
|
|
671
|
+
align,
|
|
672
|
+
keepLastDivider ? false : cellIdx === cellCount - 1
|
|
673
|
+
),
|
|
674
|
+
...stickyStyle(stickyPosition, false),
|
|
675
|
+
...columnMinWidthStyle(cell.column),
|
|
676
|
+
...(shouldWrap && {
|
|
677
|
+
whiteSpace: 'normal',
|
|
678
|
+
wordBreak: 'break-word',
|
|
679
|
+
overflowWrap: 'anywhere',
|
|
680
|
+
}),
|
|
681
|
+
...(isExpandableText && {
|
|
682
|
+
width: EXPANDABLE_CELL_MAX_WIDTH,
|
|
683
|
+
maxWidth: EXPANDABLE_CELL_MAX_WIDTH,
|
|
684
|
+
}),
|
|
685
|
+
...(isStatusBadge &&
|
|
686
|
+
!shouldWrap && {
|
|
687
|
+
whiteSpace: 'nowrap',
|
|
688
|
+
wordBreak: 'normal',
|
|
689
|
+
overflowWrap: 'normal',
|
|
690
|
+
}),
|
|
691
|
+
...extraCellStyleFromColumn(cell),
|
|
692
|
+
}}
|
|
693
|
+
>
|
|
694
|
+
<ExpandableCell
|
|
695
|
+
value={cell.value}
|
|
696
|
+
renderedCell={cell.render('Cell')}
|
|
697
|
+
column={cell.column}
|
|
698
|
+
/>
|
|
699
|
+
</td>
|
|
700
|
+
);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const renderSkeletonRows = (headers: any[], { keepLastDivider = false } = {}) =>
|
|
704
|
+
Array.from({ length: skeletonRows }).map((_, rowIdx) => {
|
|
705
|
+
const stripeBg = rowIdx % 2 === 0 ? tokens.rowBackground : tokens.rowStripeBackground;
|
|
706
|
+
return (
|
|
707
|
+
<tr key={`skel-r-${rowIdx}`} style={{ backgroundColor: stripeBg }}>
|
|
708
|
+
{headers.map((headerCol: any, colIdx: number) => (
|
|
709
|
+
<td
|
|
710
|
+
key={`skel-c-${rowIdx}-${headerCol?.id ?? colIdx}`}
|
|
711
|
+
style={{
|
|
712
|
+
...tdStyle(
|
|
713
|
+
rowIdx === skeletonRows - 1,
|
|
714
|
+
cellAlignForColumn(headerCol),
|
|
715
|
+
keepLastDivider ? false : colIdx === headers.length - 1
|
|
716
|
+
),
|
|
717
|
+
...columnMinWidthStyle(headerCol),
|
|
718
|
+
}}
|
|
719
|
+
>
|
|
720
|
+
<div
|
|
721
|
+
style={{
|
|
722
|
+
...skeletonBarStyle,
|
|
723
|
+
width: '100%',
|
|
724
|
+
}}
|
|
725
|
+
/>
|
|
726
|
+
</td>
|
|
727
|
+
))}
|
|
728
|
+
</tr>
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const renderDataRows = (
|
|
733
|
+
headers: any[],
|
|
734
|
+
{
|
|
735
|
+
disableSticky = false,
|
|
736
|
+
keyPrefix = 'row',
|
|
737
|
+
syncHover = false,
|
|
738
|
+
keepLastDivider = false,
|
|
739
|
+
}: {
|
|
740
|
+
disableSticky?: boolean;
|
|
741
|
+
keyPrefix?: string;
|
|
742
|
+
syncHover?: boolean;
|
|
743
|
+
keepLastDivider?: boolean;
|
|
744
|
+
} = {}
|
|
745
|
+
) => {
|
|
746
|
+
const headerIds = new Set(headers.map((header: any) => header.id));
|
|
747
|
+
return renderRows.map((row: any, rowIdx: number) => {
|
|
748
|
+
if (prepareRow) prepareRow(row);
|
|
749
|
+
const stripeBg = rowIdx % 2 === 0 ? tokens.rowBackground : tokens.rowStripeBackground;
|
|
750
|
+
const isHighlightedRow =
|
|
751
|
+
highlightRowClientId != null &&
|
|
752
|
+
(getHighlightRowId ?
|
|
753
|
+
String(getHighlightRowId(row?.original) ?? '')
|
|
754
|
+
: getRowClientIdForHighlight(row?.original)) === String(highlightRowClientId);
|
|
755
|
+
const baseBg = isHighlightedRow ? HIGHLIGHT_ROW_BACKGROUND : stripeBg;
|
|
756
|
+
const hoverBg = isHighlightedRow ? HIGHLIGHT_ROW_BACKGROUND_HOVER : tokens.rowHoverBackground;
|
|
757
|
+
const isHovered = syncHover && hoveredRowIndex === rowIdx;
|
|
758
|
+
const rowProps = typeof row.getRowProps === 'function' ? row.getRowProps() : {};
|
|
759
|
+
const restRowProps = { ...(rowProps ?? {}) };
|
|
760
|
+
delete (restRowProps as { key?: unknown }).key;
|
|
761
|
+
const cells = row.cells.filter((cell: any) => headerIds.has(cell.column.id));
|
|
762
|
+
return (
|
|
763
|
+
<tr
|
|
764
|
+
{...restRowProps}
|
|
765
|
+
key={`${keyPrefix}-${row.id ?? rowIdx}`}
|
|
766
|
+
style={{
|
|
767
|
+
...(restRowProps.style as React.CSSProperties | undefined),
|
|
768
|
+
backgroundColor: isHovered ? hoverBg : baseBg,
|
|
769
|
+
transition: HIGHLIGHT_ROW_TRANSITION,
|
|
770
|
+
}}
|
|
771
|
+
onMouseEnter={(e) => {
|
|
772
|
+
if (syncHover) {
|
|
773
|
+
setHoveredRowIndex(rowIdx);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
e.currentTarget.style.transition = ROW_HOVER_TRANSITION;
|
|
777
|
+
e.currentTarget.style.backgroundColor = hoverBg;
|
|
778
|
+
}}
|
|
779
|
+
onMouseLeave={(e) => {
|
|
780
|
+
if (syncHover) {
|
|
781
|
+
setHoveredRowIndex(null);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
e.currentTarget.style.transition = ROW_HOVER_TRANSITION;
|
|
785
|
+
e.currentTarget.style.backgroundColor = baseBg;
|
|
786
|
+
}}
|
|
787
|
+
>
|
|
788
|
+
{cells.map((cell: any, cellIdx: number) =>
|
|
789
|
+
renderBodyCell(cell, rowIdx, cellIdx, cells.length, { disableSticky, keepLastDivider })
|
|
790
|
+
)}
|
|
791
|
+
</tr>
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
const renderEmptyRow = (colSpan: number) => (
|
|
797
|
+
<tr>
|
|
798
|
+
<td
|
|
799
|
+
colSpan={colSpan || 1}
|
|
800
|
+
style={{
|
|
801
|
+
textAlign: 'center',
|
|
802
|
+
color: tokens.emptyStateColor,
|
|
803
|
+
fontSize: tokens.emptyStateFontSize,
|
|
804
|
+
}}
|
|
805
|
+
>
|
|
806
|
+
<div style={{ width: '100%', height: emptyStateHeight }}>
|
|
807
|
+
<img
|
|
808
|
+
src={NoDataIm}
|
|
809
|
+
alt="No data"
|
|
810
|
+
style={{
|
|
811
|
+
width: emptyStateHeight,
|
|
812
|
+
height: emptyStateHeight,
|
|
813
|
+
objectFit: 'cover',
|
|
814
|
+
placeSelf: 'center',
|
|
815
|
+
}}
|
|
816
|
+
/>
|
|
817
|
+
</div>
|
|
818
|
+
</td>
|
|
819
|
+
</tr>
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
/* -------- Render -------- */
|
|
823
|
+
if (hasFrozenSplit) {
|
|
824
|
+
return (
|
|
825
|
+
<div className={className} style={splitContainerStyle}>
|
|
826
|
+
<div ref={frozenPaneRef} style={frozenPaneStyle}>
|
|
827
|
+
<table ref={frozenTableRef} style={frozenTableStyle}>
|
|
828
|
+
{headerGroups && headerGroups.length > 0 && (
|
|
829
|
+
<thead>
|
|
830
|
+
{renderHeaderRow(frozenHeaders, {
|
|
831
|
+
headerLoading: loading,
|
|
832
|
+
disableSticky: true,
|
|
833
|
+
keepLastDivider: true,
|
|
834
|
+
})}
|
|
835
|
+
</thead>
|
|
836
|
+
)}
|
|
837
|
+
<tbody>
|
|
838
|
+
{loading ?
|
|
839
|
+
renderSkeletonRows(frozenHeaders, { keepLastDivider: true })
|
|
840
|
+
: renderDataRows(frozenHeaders, {
|
|
841
|
+
disableSticky: true,
|
|
842
|
+
keyPrefix: 'frozen',
|
|
843
|
+
syncHover: true,
|
|
844
|
+
keepLastDivider: true,
|
|
845
|
+
})
|
|
846
|
+
}
|
|
847
|
+
</tbody>
|
|
848
|
+
</table>
|
|
849
|
+
</div>
|
|
850
|
+
<div ref={scrollPaneRef} style={scrollPaneStyle}>
|
|
851
|
+
<table ref={scrollTableRef} style={scrollTableStyle}>
|
|
852
|
+
{headerGroups && headerGroups.length > 0 && (
|
|
853
|
+
<thead>
|
|
854
|
+
{renderHeaderRow(scrollHeaders, {
|
|
855
|
+
headerLoading: loading,
|
|
856
|
+
disableSticky: false,
|
|
857
|
+
})}
|
|
858
|
+
</thead>
|
|
859
|
+
)}
|
|
860
|
+
<tbody>
|
|
861
|
+
{loading ?
|
|
862
|
+
renderSkeletonRows(scrollHeaders)
|
|
863
|
+
: renderDataRows(scrollHeaders, {
|
|
864
|
+
disableSticky: false,
|
|
865
|
+
keyPrefix: 'scroll',
|
|
866
|
+
syncHover: true,
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
</tbody>
|
|
870
|
+
</table>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return (
|
|
877
|
+
<div className={className} style={containerStyle}>
|
|
878
|
+
<table style={tableStyle} {...(getTableProps ? getTableProps() : {})}>
|
|
879
|
+
{headerGroups && headerGroups.length > 0 && (loading || hasRows) && (
|
|
880
|
+
<thead>
|
|
881
|
+
{headerGroups.map((headerGroup: any) => (
|
|
882
|
+
<tr {...headerGroup.getHeaderGroupProps()}>
|
|
883
|
+
{loading ?
|
|
884
|
+
Array.from({ length: skelCols }).map((_, i) => (
|
|
885
|
+
<th
|
|
886
|
+
key={`skel-h-${i}`}
|
|
887
|
+
style={{
|
|
888
|
+
...thStyle(
|
|
889
|
+
headerAlignForColumn(
|
|
890
|
+
headerGroups?.[0]?.headers?.[i]?.column ??
|
|
891
|
+
headerGroups?.[0]?.headers?.[i]
|
|
892
|
+
),
|
|
893
|
+
i === skelCols - 1
|
|
894
|
+
),
|
|
895
|
+
...columnMinWidthStyle(
|
|
896
|
+
headerGroups?.[0]?.headers?.[i]?.column ?? headerGroups?.[0]?.headers?.[i]
|
|
897
|
+
),
|
|
898
|
+
}}
|
|
899
|
+
>
|
|
900
|
+
<div style={skeletonBarStyle} />
|
|
901
|
+
</th>
|
|
902
|
+
))
|
|
903
|
+
: headerGroup.headers.map((col: any, colIdx: number) => {
|
|
904
|
+
const isLast = colIdx === headerGroup.headers.length - 1;
|
|
905
|
+
const headerAlign = headerAlignForColumn(col);
|
|
906
|
+
const stickyPosition = stickyPositionForColumn(col);
|
|
907
|
+
return (
|
|
908
|
+
<th
|
|
909
|
+
{...col.getHeaderProps(
|
|
910
|
+
col.getSortByToggleProps ? col.getSortByToggleProps() : undefined
|
|
911
|
+
)}
|
|
912
|
+
style={{
|
|
913
|
+
...thStyle(headerAlign, isLast),
|
|
914
|
+
...stickyStyle(stickyPosition, true),
|
|
915
|
+
...columnMinWidthStyle(col),
|
|
916
|
+
...columnHeaderExtraStyle(col),
|
|
917
|
+
...(col.canSort ? { cursor: 'pointer', userSelect: 'none' } : {}),
|
|
918
|
+
}}
|
|
919
|
+
>
|
|
920
|
+
{col.render('Header')}
|
|
921
|
+
</th>
|
|
922
|
+
);
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
</tr>
|
|
926
|
+
))}
|
|
927
|
+
</thead>
|
|
928
|
+
)}
|
|
929
|
+
|
|
930
|
+
<tbody {...(getTableBodyProps ? getTableBodyProps() : {})}>
|
|
931
|
+
{loading ?
|
|
932
|
+
Array.from({ length: skeletonRows }).map((_, rowIdx) => {
|
|
933
|
+
const stripeBg = rowIdx % 2 === 0 ? tokens.rowBackground : tokens.rowStripeBackground;
|
|
934
|
+
return (
|
|
935
|
+
<tr key={`skel-r-${rowIdx}`} style={{ backgroundColor: stripeBg }}>
|
|
936
|
+
{Array.from({ length: skelCols }).map((_, colIdx) => {
|
|
937
|
+
const headerCol =
|
|
938
|
+
headerGroups?.[0]?.headers?.[colIdx]?.column ??
|
|
939
|
+
headerGroups?.[0]?.headers?.[colIdx];
|
|
940
|
+
const align = cellAlignForColumn(headerCol);
|
|
941
|
+
return (
|
|
942
|
+
<td
|
|
943
|
+
key={`skel-c-${rowIdx}-${colIdx}`}
|
|
944
|
+
style={{
|
|
945
|
+
...tdStyle(rowIdx === skeletonRows - 1, align, colIdx === skelCols - 1),
|
|
946
|
+
...columnMinWidthStyle(headerCol),
|
|
947
|
+
}}
|
|
948
|
+
>
|
|
949
|
+
<div
|
|
950
|
+
style={{
|
|
951
|
+
...skeletonBarStyle,
|
|
952
|
+
width: '100%',
|
|
953
|
+
}}
|
|
954
|
+
/>
|
|
955
|
+
</td>
|
|
956
|
+
);
|
|
957
|
+
})}
|
|
958
|
+
</tr>
|
|
959
|
+
);
|
|
960
|
+
})
|
|
961
|
+
: renderRows.length > 0 ?
|
|
962
|
+
renderDataRows(allHeaders.length ? allHeaders : (headerGroups?.[0]?.headers ?? []))
|
|
963
|
+
: renderEmptyRow(skelCols || 1)}
|
|
964
|
+
</tbody>
|
|
965
|
+
</table>
|
|
966
|
+
</div>
|
|
967
|
+
);
|
|
968
|
+
}
|