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,26 @@
1
+ import React from "react";
2
+
3
+ const MdtSidebarNav = ({ items = [], activeKey = null, onItemClick }) => {
4
+ return (
5
+ <nav className="mdt-sidebar-nav" aria-label="Master data navigation">
6
+ {items.map((item) => {
7
+ const key = item?.key;
8
+ const isActive = key === activeKey;
9
+ return (
10
+ <button
11
+ key={String(key)}
12
+ type="button"
13
+ className={`mdt-sidebar-nav-item${isActive ? " mdt-sidebar-nav-item--active" : ""}`}
14
+ onClick={() => onItemClick && onItemClick(item)}
15
+ disabled={Boolean(item?.disabled)}
16
+ title={item?.label || ""}
17
+ >
18
+ <span className="mdt-sidebar-nav-item-label">{item?.label}</span>
19
+ </button>
20
+ );
21
+ })}
22
+ </nav>
23
+ );
24
+ };
25
+
26
+ export default MdtSidebarNav;
@@ -0,0 +1,354 @@
1
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { MoreHorizontal, Pencil } from "lucide-react";
4
+
5
+ const TAB_GAP = 8;
6
+ const MORE_BTN_FALLBACK = 40;
7
+
8
+ const MdtTabsRow = ({
9
+ tabs = [],
10
+ activeTabId,
11
+ onTabClick,
12
+ onEditSavedView,
13
+ tabsConfig = {},
14
+ leadingSlot = null,
15
+ rowClassName = "",
16
+ }) => {
17
+ const {
18
+ showCounts = true,
19
+ showSavedViewEdit = true,
20
+ moreButtonAriaLabel = "More views",
21
+ className: tabsClassName = "",
22
+ } = tabsConfig;
23
+
24
+ const currentUserId = React.useMemo(() => {
25
+ try {
26
+ const userInfo = JSON.parse(localStorage.getItem("UserInfoRMS") || "{}");
27
+ return userInfo?.userId ?? null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }, []);
32
+
33
+ const isAdmin = React.useMemo(() => {
34
+ try {
35
+ const roles = localStorage.getItem("rmsRoles") || "";
36
+ return roles.includes("Admin");
37
+ } catch {
38
+ return false;
39
+ }
40
+ }, []);
41
+
42
+ const canEditTab = React.useCallback(
43
+ (tab) => {
44
+ if (!tab.isSavedView) return false;
45
+ if (isAdmin && tab.isSystem) return true;
46
+ return currentUserId != null && tab.createdBy === currentUserId;
47
+ },
48
+ [currentUserId, isAdmin],
49
+ );
50
+ const tabsRowRef = useRef(null);
51
+ const tabsMeasureRef = useRef(null);
52
+ const moreBtnRef = useRef(null);
53
+ const overflowRef = useRef(null);
54
+ const measureRetryRef = useRef(0);
55
+ const [visibleCount, setVisibleCount] = useState(tabs.length);
56
+ const [overflowOpen, setOverflowOpen] = useState(false);
57
+ const [moreBtnWidth, setMoreBtnWidth] = useState(MORE_BTN_FALLBACK);
58
+ const [menuPosition, setMenuPosition] = useState(null);
59
+
60
+ const getAvailableWidth = useCallback(() => {
61
+ const row = tabsRowRef.current;
62
+ if (!row) return 0;
63
+ const style = window.getComputedStyle(row);
64
+ const padding =
65
+ parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0");
66
+ return Math.max(0, row.clientWidth - padding);
67
+ }, []);
68
+
69
+ const measureTabs = useCallback(() => {
70
+ const measure = tabsMeasureRef.current;
71
+ if (!measure || tabs.length === 0) {
72
+ setVisibleCount(tabs.length);
73
+ return;
74
+ }
75
+
76
+ const available = getAvailableWidth();
77
+ if (available <= 0) return;
78
+
79
+ const entries = measure.querySelectorAll("[data-tab-measure]");
80
+ const widths = Array.from(entries).map((el) => el.getBoundingClientRect().width);
81
+
82
+ if (widths.length !== tabs.length || widths.some((w) => w <= 0)) {
83
+ if (measureRetryRef.current < 8) {
84
+ measureRetryRef.current += 1;
85
+ requestAnimationFrame(measureTabs);
86
+ }
87
+ return;
88
+ }
89
+ measureRetryRef.current = 0;
90
+
91
+ const reserve = (moreBtnRef.current?.offsetWidth ?? moreBtnWidth) + TAB_GAP;
92
+
93
+ let sum = 0;
94
+ for (let i = 0; i < widths.length; i++) {
95
+ sum += widths[i] + (i > 0 ? TAB_GAP : 0);
96
+ }
97
+ if (sum <= available) {
98
+ setVisibleCount(tabs.length);
99
+ return;
100
+ }
101
+
102
+ sum = 0;
103
+ let count = 0;
104
+ for (let i = 0; i < widths.length; i++) {
105
+ const w = widths[i] + (i > 0 ? TAB_GAP : 0);
106
+ if (sum + w + reserve > available) break;
107
+ sum += w;
108
+ count = i + 1;
109
+ }
110
+ setVisibleCount(Math.max(1, count));
111
+ }, [tabs, getAvailableWidth, moreBtnWidth]);
112
+
113
+ const scheduleMeasure = useCallback(() => {
114
+ requestAnimationFrame(() => {
115
+ requestAnimationFrame(measureTabs);
116
+ });
117
+ }, [measureTabs]);
118
+
119
+ useLayoutEffect(() => {
120
+ measureRetryRef.current = 0;
121
+ scheduleMeasure();
122
+ }, [scheduleMeasure, activeTabId, tabs]);
123
+
124
+ useEffect(() => {
125
+ scheduleMeasure();
126
+ }, [moreBtnWidth, scheduleMeasure]);
127
+
128
+ useEffect(() => {
129
+ const row = tabsRowRef.current;
130
+ if (!row) return undefined;
131
+
132
+ const ro = new ResizeObserver(() => scheduleMeasure());
133
+ ro.observe(row);
134
+
135
+ window.addEventListener("resize", scheduleMeasure);
136
+ return () => {
137
+ ro.disconnect();
138
+ window.removeEventListener("resize", scheduleMeasure);
139
+ };
140
+ }, [scheduleMeasure]);
141
+
142
+ useEffect(() => {
143
+ if (moreBtnRef.current) {
144
+ setMoreBtnWidth(moreBtnRef.current.offsetWidth || MORE_BTN_FALLBACK);
145
+ }
146
+ }, [overflowOpen, tabs.length]);
147
+
148
+ const updateMenuPosition = useCallback(() => {
149
+ const btn = moreBtnRef.current;
150
+ if (!btn) return;
151
+ const rect = btn.getBoundingClientRect();
152
+ setMenuPosition({
153
+ top: rect.bottom + 4,
154
+ left: rect.right,
155
+ });
156
+ }, []);
157
+
158
+ const { visibleTabs, overflowTabs } = useMemo(() => {
159
+ if (tabs.length === 0) {
160
+ return { visibleTabs: [], overflowTabs: [] };
161
+ }
162
+
163
+ const activeIdx = tabs.findIndex((t) => t.id === activeTabId);
164
+ const hasOverflow = visibleCount < tabs.length;
165
+ let visible = tabs.slice(0, visibleCount);
166
+
167
+ if (hasOverflow && activeIdx >= visibleCount) {
168
+ visible = [
169
+ ...tabs.slice(0, Math.max(0, visibleCount - 1)),
170
+ tabs[activeIdx],
171
+ ];
172
+ }
173
+
174
+ const visibleIds = new Set(visible.map((t) => t.id));
175
+ const overflow = tabs.filter((t) => !visibleIds.has(t.id));
176
+ return { visibleTabs: visible, overflowTabs: overflow };
177
+ }, [tabs, visibleCount, activeTabId]);
178
+
179
+ useLayoutEffect(() => {
180
+ if (!overflowOpen) {
181
+ setMenuPosition(null);
182
+ return undefined;
183
+ }
184
+ updateMenuPosition();
185
+ window.addEventListener("resize", updateMenuPosition);
186
+ window.addEventListener("scroll", updateMenuPosition, true);
187
+ return () => {
188
+ window.removeEventListener("resize", updateMenuPosition);
189
+ window.removeEventListener("scroll", updateMenuPosition, true);
190
+ };
191
+ }, [overflowOpen, updateMenuPosition, overflowTabs.length]);
192
+
193
+ useEffect(() => {
194
+ if (!overflowOpen) return undefined;
195
+ const onOutside = (e) => {
196
+ const inWrap = overflowRef.current?.contains(e.target);
197
+ const inMenu = e.target.closest?.(".mdt-tabs-overflow-menu--portal");
198
+ if (!inWrap && !inMenu) setOverflowOpen(false);
199
+ };
200
+ document.addEventListener("mousedown", onOutside);
201
+ return () => document.removeEventListener("mousedown", onOutside);
202
+ }, [overflowOpen]);
203
+
204
+ const renderTabEntry = (tab, { forMeasure = false } = {}) => {
205
+ const isActive = activeTabId === tab.id;
206
+ return (
207
+ <div
208
+ key={forMeasure ? `measure-${tab.id}` : tab.id}
209
+ data-tab-measure={forMeasure ? "true" : undefined}
210
+ className={`mdt-tab-entry${isActive ? " mdt-tab-entry--active" : ""}`}
211
+ >
212
+ <button
213
+ type="button"
214
+ className={`mdt-tab-item${isActive ? " mdt-tab-active" : ""}${tab.isSavedView && isActive ? " mdt-tab-item--editable" : ""}`}
215
+ onClick={() => !forMeasure && onTabClick(tab)}
216
+ >
217
+ <span className="mdt-tab-item-inner">
218
+ {tab.label}
219
+ {showCounts && tab.count !== undefined && tab.count !== null && (
220
+ <span className="mdt-tab-count">({tab.count})</span>
221
+ )}
222
+ {showSavedViewEdit && tab.isSavedView && isActive && !forMeasure && canEditTab(tab) && (
223
+ <span
224
+ role="button"
225
+ tabIndex={0}
226
+ className="mdt-tab-edit-btn"
227
+ aria-label={`Edit view ${tab.label}`}
228
+ onClick={(e) => {
229
+ e.stopPropagation();
230
+ onEditSavedView?.(tab.savedViewId);
231
+ }}
232
+ onKeyDown={(e) => {
233
+ if (e.key === "Enter" || e.key === " ") {
234
+ e.preventDefault();
235
+ e.stopPropagation();
236
+ onEditSavedView?.(tab.savedViewId);
237
+ }
238
+ }}
239
+ >
240
+ <Pencil size={11} />
241
+ </span>
242
+ )}
243
+ {showSavedViewEdit && tab.isSavedView && isActive && forMeasure && canEditTab(tab) && (
244
+ <span className="mdt-tab-edit-btn mdt-tab-edit-btn--measure" aria-hidden="true">
245
+ <Pencil size={11} />
246
+ </span>
247
+ )}
248
+ </span>
249
+ </button>
250
+ </div>
251
+ );
252
+ };
253
+
254
+ const overflowMenu =
255
+ overflowOpen && overflowTabs.length > 0 && menuPosition ? (
256
+ <div
257
+ className="mdt-tabs-overflow-menu mdt-tabs-overflow-menu--portal"
258
+ role="menu"
259
+ style={{
260
+ position: "fixed",
261
+ top: menuPosition.top,
262
+ left: menuPosition.left,
263
+ transform: "translateX(-100%)",
264
+ }}
265
+ >
266
+ {overflowTabs.map((tab) => {
267
+ const isActive = activeTabId === tab.id;
268
+ return (
269
+ <button
270
+ key={tab.id}
271
+ type="button"
272
+ role="menuitem"
273
+ className={`mdt-tabs-overflow-item${isActive ? " mdt-tabs-overflow-item--active" : ""}`}
274
+ onClick={() => {
275
+ onTabClick(tab);
276
+ setOverflowOpen(false);
277
+ }}
278
+ >
279
+ <span className="mdt-tabs-overflow-label">{tab.label}</span>
280
+ {showCounts && tab.count !== undefined && tab.count !== null && (
281
+ <span className="mdt-tabs-overflow-count">({tab.count})</span>
282
+ )}
283
+ </button>
284
+ );
285
+ })}
286
+ </div>
287
+ ) : null;
288
+
289
+ if (tabs.length === 0) return null;
290
+
291
+ const showOverflow = overflowTabs.length > 0;
292
+
293
+ return (
294
+ <div
295
+ className={[
296
+ "mdt-tabs-row",
297
+ leadingSlot ? "mdt-tabs-row--with-leading" : "",
298
+ tabsClassName,
299
+ rowClassName,
300
+ ]
301
+ .filter(Boolean)
302
+ .join(" ")}
303
+ ref={tabsRowRef}
304
+ >
305
+ {leadingSlot}
306
+ <div className="mdt-tabs-strip">
307
+ <div className="mdt-tabs-visible">
308
+ {visibleTabs.map((tab) => renderTabEntry(tab))}
309
+ </div>
310
+
311
+ {showOverflow && (
312
+ <div className="mdt-tabs-overflow-wrap" ref={overflowRef}>
313
+ <button
314
+ ref={moreBtnRef}
315
+ type="button"
316
+ className={`mdt-tabs-more-btn${overflowOpen ? " mdt-tabs-more-btn--open" : ""}`}
317
+ aria-label={moreButtonAriaLabel}
318
+ aria-expanded={overflowOpen}
319
+ onClick={() => {
320
+ setOverflowOpen((v) => {
321
+ const next = !v;
322
+ if (next) requestAnimationFrame(updateMenuPosition);
323
+ return next;
324
+ });
325
+ }}
326
+ >
327
+ <MoreHorizontal size={16} />
328
+ </button>
329
+ {overflowMenu && createPortal(overflowMenu, document.body)}
330
+ </div>
331
+ )}
332
+
333
+ {/* Hidden more button for width measurement before overflow renders */}
334
+ {!showOverflow && tabs.length > 1 && (
335
+ <button
336
+ ref={moreBtnRef}
337
+ type="button"
338
+ className="mdt-tabs-more-btn mdt-tabs-more-btn--measure"
339
+ tabIndex={-1}
340
+ aria-hidden="true"
341
+ >
342
+ <MoreHorizontal size={16} />
343
+ </button>
344
+ )}
345
+ </div>
346
+
347
+ <div className="mdt-tabs-measure" aria-hidden="true" ref={tabsMeasureRef}>
348
+ {tabs.map((tab) => renderTabEntry(tab, { forMeasure: true }))}
349
+ </div>
350
+ </div>
351
+ );
352
+ };
353
+
354
+ export default MdtTabsRow;
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import MdtTabsRow from "./MdtTabsRow";
3
+ import { resolveTabsConfig } from "./mdtFilterUtils";
4
+
5
+ /**
6
+ * Configurable tabs row wrapper (status tabs + saved views).
7
+ */
8
+ const MdtTabsSection = ({
9
+ tabs = [],
10
+ activeTabId,
11
+ onTabClick,
12
+ onEditSavedView,
13
+ tabsConfig: tabsConfigProp,
14
+ show = true,
15
+ leadingSlot = null,
16
+ rowClassName = "",
17
+ }) => {
18
+ const config = resolveTabsConfig(tabsConfigProp);
19
+
20
+ if (!show || tabs.length === 0) return null;
21
+
22
+ return (
23
+ <MdtTabsRow
24
+ tabs={tabs}
25
+ activeTabId={activeTabId}
26
+ onTabClick={onTabClick}
27
+ onEditSavedView={onEditSavedView}
28
+ tabsConfig={config}
29
+ leadingSlot={leadingSlot}
30
+ rowClassName={rowClassName}
31
+ />
32
+ );
33
+ };
34
+
35
+ export default MdtTabsSection;
@@ -0,0 +1,82 @@
1
+ import React, { useRef, useState, useEffect, useCallback } from "react";
2
+ import Tooltip from "@mui/material/Tooltip";
3
+
4
+ const TOOLTIP_SLOT_PROPS = {
5
+ tooltip: {
6
+ sx: {
7
+ bgcolor: "#ffffff",
8
+ color: "#18181b",
9
+ border: "1px solid #e4e4e7",
10
+ borderRadius: 0,
11
+ boxShadow:
12
+ "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
13
+ padding: "6px 10px",
14
+ maxWidth: 420,
15
+ fontSize: "12px",
16
+ lineHeight: 1.4,
17
+ },
18
+ },
19
+ };
20
+
21
+ /**
22
+ * Single-line ellipsis; shows tooltip only when content overflows.
23
+ */
24
+ const MdtTruncateWithTooltip = ({
25
+ children,
26
+ title,
27
+ className = "",
28
+ as: Component = "span",
29
+ ...rest
30
+ }) => {
31
+ const ref = useRef(null);
32
+ const [overflow, setOverflow] = useState(false);
33
+
34
+ const tooltipTitle =
35
+ title != null && String(title).trim() !== ""
36
+ ? String(title)
37
+ : typeof children === "string" || typeof children === "number"
38
+ ? String(children)
39
+ : "";
40
+
41
+ const measure = useCallback(() => {
42
+ const el = ref.current;
43
+ if (!el) return;
44
+ setOverflow(el.scrollWidth > el.clientWidth + 1);
45
+ }, []);
46
+
47
+ useEffect(() => {
48
+ measure();
49
+ const el = ref.current;
50
+ if (!el || typeof ResizeObserver === "undefined") return undefined;
51
+ const ro = new ResizeObserver(measure);
52
+ ro.observe(el);
53
+ return () => ro.disconnect();
54
+ }, [children, title, measure]);
55
+
56
+ const inner = (
57
+ <Component
58
+ ref={ref}
59
+ className={["mdt-cell-truncate", className].filter(Boolean).join(" ")}
60
+ {...rest}
61
+ >
62
+ {children}
63
+ </Component>
64
+ );
65
+
66
+ if (!overflow || !tooltipTitle) {
67
+ return <span className="mdt-cell-truncate-host">{inner}</span>;
68
+ }
69
+
70
+ return (
71
+ <Tooltip
72
+ title={tooltipTitle}
73
+ arrow
74
+ placement="top"
75
+ slotProps={TOOLTIP_SLOT_PROPS}
76
+ >
77
+ <span className="mdt-cell-truncate-host">{inner}</span>
78
+ </Tooltip>
79
+ );
80
+ };
81
+
82
+ export default MdtTruncateWithTooltip;