diff --git a/src/components/SharedTable.js b/src/components/SharedTable.js
index a060d632..bfa4b278 100644
--- a/src/components/SharedTable.js
+++ b/src/components/SharedTable.js
@@ -1,4 +1,80 @@
-import React, { useState, useCallback, useRef } from "react";
+import React, { useState, useCallback, useRef, useEffect } from "react";
+import ReactDOM from "react-dom";
+
+function ColumnConfigPanel({ columns, columnOrder, hiddenColumns, onReorder, onToggle, onClose }) {
+ const btnRef = useRef(null);
+ const panelRef = useRef(null);
+ const [open, setOpen] = useState(false);
+ const [menuPos, setMenuPos] = useState({ top: 0, right: 0 });
+ const [dragIdx, setDragIdx] = useState(null);
+
+ useEffect(() => {
+ const handleClick = (e) => {
+ if (btnRef.current?.contains(e.target)) return;
+ if (panelRef.current?.contains(e.target)) return;
+ setOpen(false);
+ };
+ document.addEventListener("mousedown", handleClick);
+ return () => document.removeEventListener("mousedown", handleClick);
+ }, []);
+
+ const handleOpen = () => {
+ if (btnRef.current) {
+ const rect = btnRef.current.getBoundingClientRect();
+ setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
+ }
+ setOpen(!open);
+ };
+
+ const handleDragStart = (idx) => setDragIdx(idx);
+ const handleDragOver = (e) => e.preventDefault();
+ const handleDrop = (targetIdx) => {
+ if (dragIdx === null || dragIdx === targetIdx) return;
+ const newOrder = [...columnOrder];
+ const [moved] = newOrder.splice(dragIdx, 1);
+ newOrder.splice(targetIdx, 0, moved);
+ onReorder(newOrder);
+ setDragIdx(null);
+ };
+
+ const ordered = columnOrder.map((key) => columns.find((c) => c.key === key)).filter(Boolean);
+
+ return (
+ <>
+
+ {open && ReactDOM.createPortal(
+
+
Columns
+
+ {ordered.map((col, idx) => (
+
handleDragStart(idx)}
+ onDragOver={handleDragOver}
+ onDrop={() => handleDrop(idx)}
+ >
+ ⠿
+
+
+ ))}
+
+
,
+ document.body
+ )}
+ >
+ );
+}
const SharedTable = ({
rows = [],
@@ -15,8 +91,32 @@ const SharedTable = ({
}) => {
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
const [colWidths, setColWidths] = useState(null);
+ const [columnOrder, setColumnOrder] = useState(() => columns.map((c) => c.key));
+ const [hiddenColumns, setHiddenColumns] = useState([]);
+ const [sortKey, setSortKey] = useState("lastUpdated");
+ const [sortDir, setSortDir] = useState("desc");
const gridRef = useRef(null);
+ useEffect(() => {
+ setColumnOrder((prev) => {
+ const newKeys = columns.map((c) => c.key);
+ const existing = prev.filter((k) => newKeys.includes(k));
+ const added = newKeys.filter((k) => !prev.includes(k));
+ return [...existing, ...added];
+ });
+ }, [columns]);
+
+ const visibleColumns = columnOrder
+ .map((key) => columns.find((c) => c.key === key))
+ .filter((col) => col && !hiddenColumns.includes(col.key));
+
+ const handleToggleColumn = (key) => {
+ setHiddenColumns((prev) =>
+ prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]
+ );
+ setColWidths(null);
+ };
+
const snapshotWidths = useCallback(() => {
if (!gridRef.current) return null;
const headerCells = gridRef.current.querySelectorAll("[data-col]");
@@ -35,8 +135,8 @@ const SharedTable = ({
if (!currentWidths) return;
setColWidths(currentWidths);
- const colIndex = columns.findIndex((c) => c.key === colKey);
- const nextColKey = colIndex < columns.length - 1 ? columns[colIndex + 1].key : null;
+ const colIndex = visibleColumns.findIndex((c) => c.key === colKey);
+ const nextColKey = colIndex < visibleColumns.length - 1 ? visibleColumns[colIndex + 1].key : null;
const startWidth = currentWidths[colKey] || 100;
const nextStartWidth = nextColKey ? (currentWidths[nextColKey] || 100) : 0;
@@ -63,14 +163,34 @@ const SharedTable = ({
document.body.style.userSelect = "none";
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
- }, [snapshotWidths, columns]);
+ }, [snapshotWidths, visibleColumns]);
+
+ const handleSort = (key) => {
+ if (sortKey === key) {
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"));
+ } else {
+ setSortKey(key);
+ setSortDir("asc");
+ }
+ };
+
+ const sortedRows = [...rows].sort((a, b) => {
+ if (!sortKey) return 0;
+ let aVal = a[sortKey] ?? "";
+ let bVal = b[sortKey] ?? "";
+ if (typeof aVal === "string") aVal = aVal.toLowerCase();
+ if (typeof bVal === "string") bVal = bVal.toLowerCase();
+ if (aVal < bVal) return sortDir === "asc" ? -1 : 1;
+ if (aVal > bVal) return sortDir === "asc" ? 1 : -1;
+ return 0;
+ });
if (loading) return Loading...
;
if (error) return {error}
;
const gridTemplate = colWidths
- ? ["40px", ...columns.map((col) => `${colWidths[col.key] || 100}px`), "80px"].join(" ")
- : ["40px", ...columns.map(() => "1fr"), "80px"].join(" ");
+ ? ["40px", ...visibleColumns.map((col) => `${colWidths[col.key] || 100}px`), "80px"].join(" ")
+ : ["40px", ...visibleColumns.map(() => "1fr"), "80px"].join(" ");
const paginationBar = (
@@ -100,6 +220,13 @@ const SharedTable = ({
+ { setColumnOrder(order); setColWidths(null); }}
+ onToggle={handleToggleColumn}
+ />
);
@@ -112,23 +239,29 @@ const SharedTable = ({
- {columns.map((col) => (
-
+ {visibleColumns.map((col) => (
+
handleSort(col.key)}
+ >
{col.label}
+ {sortKey === col.key && {sortDir === "asc" ? " ▲" : " ▼"}}
handleMouseDown(e, col.key)}
+ onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, col.key); }}
/>
))}
Actions
- {rows.map((row, idx) => (
+ {sortedRows.map((row, idx) => (
- {columns.map((col) => (
+ {visibleColumns.map((col) => (
{col.render ? col.render(row[col.key], row) : row[col.key]}
@@ -151,7 +284,6 @@ const SharedTable = ({
))}
-
);
};
diff --git a/src/site.css b/src/site.css
index 59c25f28..17058f35 100644
--- a/src/site.css
+++ b/src/site.css
@@ -194,6 +194,153 @@ body {
text-decoration: underline;
}
+.colConfigBtn {
+ font-size: 15px;
+ color: #999;
+}
+
+.colConfigPanel {
+ position: fixed;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
+ z-index: 99999;
+ width: 220px;
+ overflow: hidden;
+}
+
+.colConfigTitle {
+ padding: 10px 14px;
+ font-size: 12px;
+ font-weight: 600;
+ color: #999;
+ text-transform: uppercase;
+ border-bottom: 1px solid #eee;
+}
+
+.colConfigList {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.colConfigItem {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 14px;
+ cursor: grab;
+ border-bottom: 1px solid #f5f5f5;
+ font-size: 13px;
+}
+
+.colConfigItem:hover {
+ background: #f8f9fa;
+}
+
+.colConfigItem.dragging {
+ opacity: 0.4;
+}
+
+.colConfigDragHandle {
+ color: #ccc;
+ font-size: 14px;
+ cursor: grab;
+ flex-shrink: 0;
+}
+
+.colConfigLabel {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ flex: 1;
+}
+
+.colConfigLabel input[type="checkbox"] {
+ width: 14px;
+ height: 14px;
+}
+
+.gridSortable {
+ cursor: pointer;
+ user-select: none;
+}
+
+.gridSortable:hover {
+ background: #eef1f4;
+}
+
+.gridHeader.sorted {
+ background: #e8edf2;
+}
+
+.sortArrow {
+ font-size: 10px;
+ color: #007bff;
+ margin-left: 4px;
+}
+
+.colConfigBtn {
+ margin-right: 8px;
+}
+
+.colConfigPanel {
+ position: fixed;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
+ z-index: 99999;
+ width: 220px;
+ overflow: hidden;
+}
+
+.colConfigTitle {
+ padding: 10px 14px;
+ font-size: 13px;
+ font-weight: 600;
+ color: #333;
+ border-bottom: 1px solid #eee;
+}
+
+.colConfigList {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.colConfigItem {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 14px;
+ font-size: 13px;
+ cursor: grab;
+ border-bottom: 1px solid #f5f5f5;
+}
+
+.colConfigItem:hover {
+ background: #f8f9fa;
+}
+
+.colConfigItem.dragging {
+ opacity: 0.4;
+}
+
+.colConfigDragHandle {
+ color: #ccc;
+ font-size: 14px;
+ cursor: grab;
+}
+
+.colConfigLabel {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ flex: 1;
+}
+
.colResizeHandle {
position: absolute;
top: 0;