@rufous/ui 0.1.68 → 0.1.69

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.
@@ -26,22 +26,31 @@ function DataGrid({
26
26
  pageSizeOptions = [5, 10, 25, 50],
27
27
  title
28
28
  }) {
29
- const initialColumns = useMemo(() => initialColumnsProp.map((col) => ({
30
- ...col,
31
- key: col.key || col.field || "",
32
- header: col.header || col.headerName || ""
33
- })), [initialColumnsProp]);
34
- const [columns, setColumns] = useState(initialColumns);
29
+ const [columnOverrides, setColumnOverrides] = useState({});
30
+ const resolvedColumns = useMemo(() => {
31
+ return initialColumnsProp.map((col) => {
32
+ const field = String(col.field || col.key || "");
33
+ const override = columnOverrides[field] || {};
34
+ return {
35
+ ...col,
36
+ field,
37
+ headerName: col.headerName || col.header || "",
38
+ hidden: override.hidden !== void 0 ? override.hidden : col.hidden,
39
+ pinned: override.pinned !== void 0 ? override.pinned : col.pinned
40
+ };
41
+ });
42
+ }, [initialColumnsProp, columnOverrides]);
35
43
  const [columnWidths, setColumnWidths] = useState(() => {
36
44
  const widths = {};
37
- initialColumns.forEach((col) => {
45
+ initialColumnsProp.forEach((col) => {
46
+ const field = String(col.field || col.key || "");
38
47
  const w = col.width || 200;
39
- widths[String(col.key)] = typeof w === "number" ? w : parseInt(w);
48
+ widths[field] = typeof w === "number" ? w : parseInt(w);
40
49
  });
41
50
  return widths;
42
51
  });
43
52
  const [pageSize, setPageSize] = useState(initialPageSize);
44
- const [sortKey, setSortKey] = useState(null);
53
+ const [sortField, setSortField] = useState(null);
45
54
  const [sortDirection, setSortDirection] = useState(null);
46
55
  const [filterText, setFilterText] = useState("");
47
56
  const [currentPage, setCurrentPage] = useState(1);
@@ -54,40 +63,15 @@ function DataGrid({
54
63
  const menuRef = useRef(null);
55
64
  const [showManageColumns, setShowManageColumns] = useState(false);
56
65
  const [showAdvancedFilter, setShowAdvancedFilter] = useState(false);
66
+ const initialFilterCol = String(initialColumnsProp[0]?.field || initialColumnsProp[0]?.key || "");
57
67
  const [advancedFilters, setAdvancedFilters] = useState([
58
- { column: String(initialColumns[0]?.key || ""), operator: "contains", value: "", logic: "AND" }
68
+ { column: initialFilterCol, operator: "contains", value: "", logic: "AND" }
59
69
  ]);
60
- useEffect(() => {
61
- setColumns((prevColumns) => {
62
- return initialColumns.map((newCol) => {
63
- const prevCol = prevColumns.find((c) => (c.key || c.field) === (newCol.key || newCol.field));
64
- if (!prevCol) return newCol;
65
- return {
66
- ...newCol,
67
- hidden: prevCol.hidden !== void 0 ? prevCol.hidden : newCol.hidden,
68
- pinned: prevCol.pinned !== void 0 ? prevCol.pinned : newCol.pinned
69
- };
70
- });
71
- });
72
- }, [initialColumns]);
73
- useEffect(() => {
74
- setColumnWidths((prev) => {
75
- const next = { ...prev };
76
- initialColumns.forEach((col) => {
77
- const key = String(col.key);
78
- if (next[key] === void 0) {
79
- const w = col.width || 200;
80
- next[key] = typeof w === "number" ? w : parseInt(w);
81
- }
82
- });
83
- return next;
84
- });
85
- }, [initialColumns]);
86
70
  const [colSearch, setColSearch] = useState("");
87
71
  useEffect(() => {
88
72
  const handleMouseMove = (e) => {
89
73
  if (!resizingColumn) return;
90
- const col = columns.find((c) => String(c.key) === resizingColumn);
74
+ const col = resolvedColumns.find((c) => String(c.field) === resizingColumn);
91
75
  const diff = e.clientX - startX;
92
76
  const minW = col?.minWidth ? typeof col.minWidth === "number" ? col.minWidth : parseInt(col.minWidth) : 80;
93
77
  const maxW = col?.maxWidth ? typeof col.maxWidth === "number" ? col.maxWidth : parseInt(col.maxWidth) : Infinity;
@@ -103,7 +87,7 @@ function DataGrid({
103
87
  document.removeEventListener("mousemove", handleMouseMove);
104
88
  document.removeEventListener("mouseup", handleMouseUp);
105
89
  };
106
- }, [resizingColumn, startX, startWidth, columns]);
90
+ }, [resizingColumn, startX, startWidth, resolvedColumns]);
107
91
  useEffect(() => {
108
92
  const handleClickOutside = (e) => {
109
93
  if (menuRef.current && !menuRef.current.contains(e.target)) {
@@ -113,32 +97,55 @@ function DataGrid({
113
97
  document.addEventListener("mousedown", handleClickOutside);
114
98
  return () => document.removeEventListener("mousedown", handleClickOutside);
115
99
  }, []);
116
- const handleSort = (key, dir) => {
100
+ useEffect(() => {
101
+ setColumnWidths((prev) => {
102
+ const next = { ...prev };
103
+ initialColumnsProp.forEach((col) => {
104
+ const field = String(col.field || col.key || "");
105
+ if (next[field] === void 0) {
106
+ const w = col.width || 200;
107
+ next[field] = typeof w === "number" ? w : parseInt(w);
108
+ }
109
+ });
110
+ return next;
111
+ });
112
+ }, [initialColumnsProp]);
113
+ const handleSort = (fieldKey, dir) => {
117
114
  if (dir !== void 0) {
118
- setSortKey(key);
115
+ setSortField(fieldKey);
119
116
  setSortDirection(dir);
120
- } else if (sortKey === key) {
117
+ } else if (sortField === fieldKey) {
121
118
  if (sortDirection === "asc") setSortDirection("desc");
122
119
  else {
123
- setSortKey(null);
120
+ setSortField(null);
124
121
  setSortDirection(null);
125
122
  }
126
123
  } else {
127
- setSortKey(key);
124
+ setSortField(fieldKey);
128
125
  setSortDirection("asc");
129
126
  }
130
127
  setActiveMenu(null);
131
128
  };
132
- const togglePin = (key, side) => {
133
- setColumns((prev) => prev.map(
134
- (col) => String(col.key) === key ? { ...col, pinned: col.pinned === side ? void 0 : side } : col
135
- ));
129
+ const togglePin = (fieldKey, side) => {
130
+ setColumnOverrides((prev) => {
131
+ const current = prev[fieldKey] || {};
132
+ return {
133
+ ...prev,
134
+ [fieldKey]: { ...current, pinned: current.pinned === side ? void 0 : side }
135
+ };
136
+ });
136
137
  setActiveMenu(null);
137
138
  };
138
- const toggleHide = (key) => {
139
- setColumns((prev) => prev.map(
140
- (col) => String(col.key) === key ? { ...col, hidden: !col.hidden } : col
141
- ));
139
+ const toggleHide = (fieldKey) => {
140
+ setColumnOverrides((prev) => {
141
+ const current = prev[fieldKey] || {};
142
+ const col = resolvedColumns.find((c) => String(c.field) === fieldKey);
143
+ if (col?.hideable === false) return prev;
144
+ return {
145
+ ...prev,
146
+ [fieldKey]: { ...current, hidden: !current.hidden }
147
+ };
148
+ });
142
149
  setActiveMenu(null);
143
150
  };
144
151
  const filteredData = useMemo(() => {
@@ -180,30 +187,30 @@ function DataGrid({
180
187
  });
181
188
  }, [data, filterText, advancedFilters]);
182
189
  const sortedData = useMemo(() => {
183
- if (!sortKey || !sortDirection) return filteredData;
184
- const col = columns.find((c) => c.key === sortKey);
190
+ if (!sortField || !sortDirection) return filteredData;
191
+ const col = resolvedColumns.find((c) => c.field === sortField);
185
192
  return [...filteredData].sort((a, b) => {
186
- let aVal = a[sortKey];
187
- let bVal = b[sortKey];
193
+ let aVal = a[sortField];
194
+ let bVal = b[sortField];
188
195
  if (col?.valueGetter) {
189
- aVal = col.valueGetter({ value: aVal, row: a, field: String(sortKey) });
190
- bVal = col.valueGetter({ value: bVal, row: b, field: String(sortKey) });
196
+ aVal = col.valueGetter({ value: aVal, row: a, field: String(sortField) });
197
+ bVal = col.valueGetter({ value: bVal, row: b, field: String(sortField) });
191
198
  }
192
199
  if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
193
200
  if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
194
201
  return 0;
195
202
  });
196
- }, [filteredData, sortKey, sortDirection, columns]);
203
+ }, [filteredData, sortField, sortDirection, resolvedColumns]);
197
204
  const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
198
205
  const paginatedData = useMemo(() => {
199
206
  const start = (currentPage - 1) * pageSize;
200
207
  return sortedData.slice(start, start + pageSize);
201
208
  }, [sortedData, currentPage, pageSize]);
202
209
  const handleExport = () => {
203
- const visibleCols = columns.filter((c) => !c.hidden);
204
- const headers = visibleCols.map((c) => c.header).join(",");
210
+ const visibleCols = resolvedColumns.filter((c) => !c.hidden);
211
+ const headers = visibleCols.map((c) => c.headerName).join(",");
205
212
  const rows = sortedData.map(
206
- (item) => visibleCols.map((c) => `"${String(item[c.key]).replace(/"/g, '""')}"`).join(",")
213
+ (item) => visibleCols.map((c) => `"${String(item[c.field]).replace(/"/g, '""')}"`).join(",")
207
214
  );
208
215
  const csv = [headers, ...rows].join("\n");
209
216
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
@@ -214,24 +221,24 @@ function DataGrid({
214
221
  link.click();
215
222
  document.body.removeChild(link);
216
223
  };
217
- const handleMenuOpen = (e, key) => {
224
+ const handleMenuOpen = (e, keyStr) => {
218
225
  e.stopPropagation();
219
226
  const rect = e.currentTarget.getBoundingClientRect();
220
227
  setMenuPosition({ top: rect.bottom + 4, left: rect.left });
221
- setActiveMenu(key);
228
+ setActiveMenu(keyStr);
222
229
  };
223
230
  const visibleColumns = useMemo(() => {
224
- const left = columns.filter((c) => !c.hidden && c.pinned === "left");
225
- const mid = columns.filter((c) => !c.hidden && !c.pinned);
226
- const right = columns.filter((c) => !c.hidden && c.pinned === "right");
231
+ const left = resolvedColumns.filter((c) => !c.hidden && c.pinned === "left");
232
+ const mid = resolvedColumns.filter((c) => !c.hidden && !c.pinned);
233
+ const right = resolvedColumns.filter((c) => !c.hidden && c.pinned === "right");
227
234
  return [...left, ...mid, ...right];
228
- }, [columns]);
235
+ }, [resolvedColumns]);
229
236
  const getLeftOffset = (col, idx) => {
230
237
  if (col.pinned !== "left") return void 0;
231
238
  let offset = 0;
232
239
  for (let i = 0; i < idx; i++) {
233
240
  if (visibleColumns[i].pinned === "left") {
234
- offset += columnWidths[String(visibleColumns[i].key)] || 200;
241
+ offset += columnWidths[String(visibleColumns[i].field)] || 200;
235
242
  }
236
243
  }
237
244
  return offset;
@@ -265,14 +272,14 @@ function DataGrid({
265
272
  },
266
273
  /* @__PURE__ */ React.createElement(Columns, { size: 16 })
267
274
  ), /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: handleExport }, /* @__PURE__ */ React.createElement(Download, { size: 14 }), " Export CSV"))), /* @__PURE__ */ React.createElement("div", { className: "dg-table-wrap" }, /* @__PURE__ */ React.createElement("table", { className: "dg-table" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, visibleColumns.map((col, idx) => {
268
- const colKey = String(col.key);
269
- const width = columnWidths[colKey] || 200;
275
+ const colField = String(col.field);
276
+ const width = columnWidths[colField] || 200;
270
277
  const leftOffset = getLeftOffset(col, idx);
271
- const isSorted = sortKey === col.key;
278
+ const isSorted = sortField === col.field;
272
279
  return /* @__PURE__ */ React.createElement(
273
280
  "th",
274
281
  {
275
- key: colKey,
282
+ key: colField,
276
283
  className: `dg-thead-cell${col.pinned === "left" ? " pinned-left" : col.pinned === "right" ? " pinned-right" : ""} ${col.headerClassName || ""}`,
277
284
  style: { width, minWidth: width, left: leftOffset, flex: col.flex }
278
285
  },
@@ -280,25 +287,25 @@ function DataGrid({
280
287
  "div",
281
288
  {
282
289
  className: `dg-th-label${col.sortable === false ? " no-sort" : ""}`,
283
- onClick: () => col.sortable !== false && handleSort(col.key)
290
+ onClick: () => col.sortable !== false && handleSort(col.field || "")
284
291
  },
285
- col.header,
292
+ col.headerName,
286
293
  isSorted && sortDirection === "asc" && /* @__PURE__ */ React.createElement(ChevronUp, { size: 12 }),
287
294
  isSorted && sortDirection === "desc" && /* @__PURE__ */ React.createElement(ChevronDown, { size: 12 })
288
295
  ), /* @__PURE__ */ React.createElement("div", { className: "dg-th-actions" }, !col.disableColumnMenu && /* @__PURE__ */ React.createElement(
289
296
  "button",
290
297
  {
291
298
  className: "dg-th-menu-btn",
292
- onClick: (e) => handleMenuOpen(e, colKey)
299
+ onClick: (e) => handleMenuOpen(e, colField)
293
300
  },
294
301
  /* @__PURE__ */ React.createElement(MoreVertical, { size: 13 })
295
302
  ), /* @__PURE__ */ React.createElement(
296
303
  "div",
297
304
  {
298
- className: `dg-resizer${resizingColumn === colKey ? " resizing" : ""}`,
305
+ className: `dg-resizer${resizingColumn === colField ? " resizing" : ""}`,
299
306
  onMouseDown: (e) => {
300
307
  e.preventDefault();
301
- setResizingColumn(colKey);
308
+ setResizingColumn(colField);
302
309
  setStartX(e.clientX);
303
310
  setStartWidth(width);
304
311
  }
@@ -306,19 +313,19 @@ function DataGrid({
306
313
  )))
307
314
  );
308
315
  }), actions && /* @__PURE__ */ React.createElement("th", { style: { width: 0, padding: 0 } }))), /* @__PURE__ */ React.createElement("tbody", null, paginatedData.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: visibleColumns.length + (actions ? 1 : 0), className: "dg-empty" }, "No records found")) : paginatedData.map((item) => /* @__PURE__ */ React.createElement("tr", { key: item.id, className: "dg-tbody-row" }, visibleColumns.map((col, idx) => {
309
- const colKey = String(col.key);
310
- const width = columnWidths[colKey] || 200;
316
+ const colField = String(col.field);
317
+ const width = columnWidths[colField] || 200;
311
318
  const leftOffset = getLeftOffset(col, idx);
312
319
  return /* @__PURE__ */ React.createElement(
313
320
  "td",
314
321
  {
315
- key: `${item.id}-${colKey}`,
322
+ key: `${item.id}-${colField}`,
316
323
  className: `dg-td${col.pinned === "left" ? " pinned-left" : ""} ${col.cellClassName || ""}`,
317
324
  style: { width, minWidth: width, maxWidth: width, left: leftOffset, flex: col.flex }
318
325
  },
319
326
  (() => {
320
- const field = String(col.key);
321
- const rawValue = item[col.key];
327
+ const field = String(col.field);
328
+ const rawValue = item[col.field || ""];
322
329
  let value = col.valueGetter ? col.valueGetter({ value: rawValue, row: item, field }) : rawValue;
323
330
  const formattedValue = col.valueFormatter ? col.valueFormatter({ value, row: item, field }) : value;
324
331
  if (col.renderCell) {
@@ -386,7 +393,27 @@ function DataGrid({
386
393
  value: colSearch,
387
394
  onChange: (e) => setColSearch(e.target.value)
388
395
  }
389
- )), columns.filter((c) => c.header.toLowerCase().includes(colSearch.toLowerCase()) && c.hideable !== false).map((col) => /* @__PURE__ */ React.createElement("div", { key: String(col.key), className: "dg-col-row" }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-label" }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-dot", style: { background: col.hidden ? "var(--border-color)" : "var(--primary-color)" } }), col.header), /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => toggleHide(String(col.key)) }, col.hidden ? /* @__PURE__ */ React.createElement(EyeOff, { size: 14 }) : /* @__PURE__ */ React.createElement(EyeOff, { size: 14, style: { opacity: 0.4 } }))))), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-footer" }, /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => setColumns((p) => p.map((c) => ({ ...c, hidden: false }))) }, "Show All"), /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => setColumns((p) => p.map((c) => c.hideable !== false ? { ...c, hidden: true } : c)) }, "Hide All")))), showAdvancedFilter && /* @__PURE__ */ React.createElement("div", { className: "dg-modal-overlay", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal dg-modal-wide", onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal-header" }, /* @__PURE__ */ React.createElement("h3", null, "Filters"), /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement(X, { size: 18 }))), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-body" }, advancedFilters.map((f, idx) => /* @__PURE__ */ React.createElement("div", { key: idx }, idx > 0 && /* @__PURE__ */ React.createElement("div", { className: "dg-filter-logic" }, /* @__PURE__ */ React.createElement(
396
+ )), resolvedColumns.filter((c) => c.header.toLowerCase().includes(colSearch.toLowerCase())).map((col) => {
397
+ const key = String(col.key);
398
+ const isUnhideable = col.hideable === false;
399
+ return /* @__PURE__ */ React.createElement("div", { key, className: `dg-col-row${isUnhideable ? " disabled" : ""}` }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-label" }, /* @__PURE__ */ React.createElement("div", { className: "dg-col-dot", style: { background: col.hidden ? "var(--border-color)" : "var(--primary-color)" } }), col.header), !isUnhideable && /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => toggleHide(key) }, col.hidden ? /* @__PURE__ */ React.createElement(EyeOff, { size: 14 }) : /* @__PURE__ */ React.createElement(EyeOff, { size: 14, style: { opacity: 0.4 } })));
400
+ })), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-footer" }, /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => setColumnOverrides((prev) => {
401
+ const next = { ...prev };
402
+ resolvedColumns.forEach((c) => {
403
+ const k = String(c.key);
404
+ next[k] = { ...next[k], hidden: false };
405
+ });
406
+ return next;
407
+ }) }, "Show All"), /* @__PURE__ */ React.createElement("button", { className: "dg-action-btn", onClick: () => {
408
+ const newOverrides = { ...columnOverrides };
409
+ resolvedColumns.forEach((c) => {
410
+ if (c.hideable !== false) {
411
+ const key = String(c.key);
412
+ newOverrides[key] = { ...newOverrides[key], hidden: true };
413
+ }
414
+ });
415
+ setColumnOverrides(newOverrides);
416
+ } }, "Hide All")))), showAdvancedFilter && /* @__PURE__ */ React.createElement("div", { className: "dg-modal-overlay", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal dg-modal-wide", onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "dg-modal-header" }, /* @__PURE__ */ React.createElement("h3", null, "Filters"), /* @__PURE__ */ React.createElement("button", { className: "dg-icon-btn", onClick: () => setShowAdvancedFilter(false) }, /* @__PURE__ */ React.createElement(X, { size: 18 }))), /* @__PURE__ */ React.createElement("div", { className: "dg-modal-body" }, advancedFilters.map((f, idx) => /* @__PURE__ */ React.createElement("div", { key: idx }, idx > 0 && /* @__PURE__ */ React.createElement("div", { className: "dg-filter-logic" }, /* @__PURE__ */ React.createElement(
390
417
  "button",
391
418
  {
392
419
  className: `dg-logic-btn${f.logic === "AND" ? " active" : ""}`,
@@ -407,7 +434,7 @@ function DataGrid({
407
434
  value: f.column,
408
435
  onChange: (e) => setAdvancedFilters((p) => p.map((fi, i) => i === idx ? { ...fi, column: e.target.value } : fi))
409
436
  },
410
- columns.map((c) => /* @__PURE__ */ React.createElement("option", { key: String(c.key), value: String(c.key) }, c.header))
437
+ resolvedColumns.map((c) => /* @__PURE__ */ React.createElement("option", { key: String(c.key), value: String(c.key) }, c.header))
411
438
  ), /* @__PURE__ */ React.createElement(
412
439
  "select",
413
440
  {
@@ -434,7 +461,7 @@ function DataGrid({
434
461
  {
435
462
  className: "dg-action-btn",
436
463
  style: { alignSelf: "flex-start", marginTop: 4 },
437
- onClick: () => setAdvancedFilters((p) => [...p, { column: String(columns[0].key), operator: "contains", value: "", logic: "AND" }])
464
+ onClick: () => setAdvancedFilters((p) => [...p, { column: String(resolvedColumns[0].key), operator: "contains", value: "", logic: "AND" }])
438
465
  },
439
466
  /* @__PURE__ */ React.createElement(Plus, { size: 14 }),
440
467
  " Add Filter"
@@ -442,7 +469,7 @@ function DataGrid({
442
469
  "button",
443
470
  {
444
471
  className: "dg-action-btn",
445
- onClick: () => setAdvancedFilters([{ column: String(columns[0].key), operator: "contains", value: "", logic: "AND" }])
472
+ onClick: () => setAdvancedFilters([{ column: String(resolvedColumns[0].key), operator: "contains", value: "", logic: "AND" }])
446
473
  },
447
474
  /* @__PURE__ */ React.createElement(Trash2, { size: 14 }),
448
475
  " Reset"