638 lines
21 KiB
JavaScript
638 lines
21 KiB
JavaScript
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
|
|
import SearchOutlined from "@mui/icons-material/SearchOutlined";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import MenuList from "@mui/material/MenuList";
|
|
import Popover from "@mui/material/Popover";
|
|
import TextField from "@mui/material/TextField";
|
|
import Tooltip from "@mui/material/Tooltip";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { useTimeZone } from "@/app/timezone/TimeZoneProvider.jsx";
|
|
import { Button } from "@/shared/ui/Button.jsx";
|
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
|
|
|
const DEFAULT_COLUMN_WIDTH = "minmax(120px, 1fr)";
|
|
const DEFAULT_RESIZE_MIN_WIDTH = 72;
|
|
const DEFAULT_RESIZE_MAX_WIDTH = 960;
|
|
const ACTION_COLUMN_WIDTH = "112px";
|
|
const DEFAULT_COMPACT_MIN_WIDTH = 220;
|
|
const LOW_PRIORITY_COMPACT_MIN_WIDTH = 180;
|
|
|
|
export function DataTable({
|
|
className = "",
|
|
columns,
|
|
context,
|
|
emptyLabel = "当前无数据",
|
|
fixedEdges = true,
|
|
getRowProps,
|
|
infiniteScroll,
|
|
items,
|
|
minWidth = "980px",
|
|
onColumnWidthsChange,
|
|
pagination,
|
|
resizableColumns = true,
|
|
rowKey,
|
|
tableClassName = "",
|
|
title,
|
|
total
|
|
}) {
|
|
const { timeZone } = useTimeZone();
|
|
const [columnWidths, setColumnWidths] = useState({});
|
|
const [filterMenu, setFilterMenu] = useState({ anchorEl: null, columnKey: "" });
|
|
const [filterQuery, setFilterQuery] = useState("");
|
|
const [hasHorizontalOverflow, setHasHorizontalOverflow] = useState(false);
|
|
const [paginationLoadingMore, setPaginationLoadingMore] = useState(false);
|
|
const [resizeSession, setResizeSession] = useState(null);
|
|
const loadMoreRef = useRef(null);
|
|
const scrollRef = useRef(null);
|
|
const safeItems = Array.isArray(items) ? items : [];
|
|
const paginationTotal = Number(pagination?.total || 0);
|
|
const paginationHasMore =
|
|
Boolean(pagination?.onPageChange) &&
|
|
(Number.isFinite(paginationTotal) && paginationTotal > 0
|
|
? safeItems.length < paginationTotal
|
|
: Boolean(pagination?.hasNextPage));
|
|
const infiniteLoading = Boolean(infiniteScroll?.loading || paginationLoadingMore);
|
|
const infiniteHasMore = Boolean(infiniteScroll?.hasMore || paginationHasMore);
|
|
const infiniteLoadLabel = infiniteScroll?.loadingLabel || "加载中...";
|
|
const infiniteDoneLabel = infiniteScroll?.doneLabel || "";
|
|
const loadMore = useCallback(() => {
|
|
if (infiniteScroll?.onLoadMore) {
|
|
infiniteScroll.onLoadMore();
|
|
return;
|
|
}
|
|
pagination?.onPageChange?.(Number(pagination?.page || 1) + 1);
|
|
}, [infiniteScroll, pagination]);
|
|
const renderContext = useMemo(() => (context ? { ...context, timeZone } : { timeZone }), [context, timeZone]);
|
|
|
|
const preparedColumns = useMemo(
|
|
() =>
|
|
columns.map((column) => {
|
|
const normalizedColumn = normalizeColumn(column);
|
|
return {
|
|
...normalizedColumn,
|
|
fixed: resolveColumnFixed(normalizedColumn, fixedEdges && hasHorizontalOverflow),
|
|
width: columnWidths[normalizedColumn.key] || normalizedColumn.width || DEFAULT_COLUMN_WIDTH
|
|
};
|
|
}),
|
|
[columnWidths, columns, fixedEdges, hasHorizontalOverflow]
|
|
);
|
|
|
|
const gridTemplate = preparedColumns.map((column) => column.width).join(" ");
|
|
const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null;
|
|
const activeFilter = activeFilterColumn?.filter || null;
|
|
const activeFilterIsText = activeFilter?.type === "text";
|
|
const filterColumns = preparedColumns.filter((column) => column.filter);
|
|
const hasColumnFilters = filterColumns.length > 0;
|
|
const hasActiveColumnFilters = filterColumns.some((column) => isFilterActive(column.filter));
|
|
const filterOptions = useMemo(() => filterVisibleOptions(activeFilter, filterQuery), [activeFilter, filterQuery]);
|
|
|
|
useEffect(() => {
|
|
const scrollNode = scrollRef.current;
|
|
if (!scrollNode) {
|
|
return undefined;
|
|
}
|
|
|
|
const measureOverflow = () => {
|
|
const nextHasOverflow = scrollNode.scrollWidth > scrollNode.clientWidth + 1;
|
|
setHasHorizontalOverflow((current) => (current === nextHasOverflow ? current : nextHasOverflow));
|
|
};
|
|
|
|
measureOverflow();
|
|
window.addEventListener("resize", measureOverflow);
|
|
|
|
if (typeof ResizeObserver === "undefined") {
|
|
return () => {
|
|
window.removeEventListener("resize", measureOverflow);
|
|
};
|
|
}
|
|
|
|
const observer = new ResizeObserver(measureOverflow);
|
|
observer.observe(scrollNode);
|
|
if (scrollNode.firstElementChild) {
|
|
observer.observe(scrollNode.firstElementChild);
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
window.removeEventListener("resize", measureOverflow);
|
|
};
|
|
}, [gridTemplate, minWidth, safeItems.length]);
|
|
|
|
useEffect(() => {
|
|
if (!resizeSession) {
|
|
return undefined;
|
|
}
|
|
|
|
const moveColumn = (event) => {
|
|
const delta = event.clientX - resizeSession.startX;
|
|
const nextWidth = clamp(resizeSession.startWidth + delta, resizeSession.minWidth, resizeSession.maxWidth);
|
|
const cssWidth = `${Math.round(nextWidth)}px`;
|
|
|
|
setColumnWidths((current) => {
|
|
if (current[resizeSession.key] === cssWidth) {
|
|
return current;
|
|
}
|
|
const next = { ...current, [resizeSession.key]: cssWidth };
|
|
onColumnWidthsChange?.(next);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const finishResize = () => {
|
|
setResizeSession(null);
|
|
};
|
|
|
|
document.body.style.cursor = "col-resize";
|
|
document.body.style.userSelect = "none";
|
|
window.addEventListener("pointermove", moveColumn);
|
|
window.addEventListener("pointerup", finishResize, { once: true });
|
|
window.addEventListener("pointercancel", finishResize, { once: true });
|
|
|
|
return () => {
|
|
document.body.style.cursor = "";
|
|
document.body.style.userSelect = "";
|
|
window.removeEventListener("pointermove", moveColumn);
|
|
window.removeEventListener("pointerup", finishResize);
|
|
window.removeEventListener("pointercancel", finishResize);
|
|
};
|
|
}, [onColumnWidthsChange, resizeSession]);
|
|
|
|
useEffect(() => {
|
|
const root = scrollRef.current;
|
|
const target = loadMoreRef.current;
|
|
if (!root || !target || !loadMore || !infiniteHasMore || infiniteLoading) {
|
|
return undefined;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries.some((entry) => entry.isIntersecting)) {
|
|
setPaginationLoadingMore(Boolean(paginationHasMore));
|
|
loadMore();
|
|
}
|
|
},
|
|
{ root, rootMargin: "0px 0px 96px", threshold: 0.01 }
|
|
);
|
|
|
|
observer.observe(target);
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, [infiniteHasMore, infiniteLoading, loadMore, paginationHasMore, safeItems.length]);
|
|
|
|
useEffect(() => {
|
|
setPaginationLoadingMore(false);
|
|
}, [safeItems.length]);
|
|
|
|
const startResize = (event, column) => {
|
|
if (!resizableColumns || column.resizable === false) {
|
|
return;
|
|
}
|
|
|
|
const headerCell = event.currentTarget.closest("[data-table-header-cell]");
|
|
const startWidth = headerCell?.getBoundingClientRect().width;
|
|
if (!startWidth) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setResizeSession({
|
|
key: column.key,
|
|
maxWidth: resolveResizeMaxWidth(column),
|
|
minWidth: resolveResizeMinWidth(column),
|
|
startWidth,
|
|
startX: event.clientX
|
|
});
|
|
};
|
|
|
|
const openColumnFilter = (event, column) => {
|
|
if (!column.filter) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
setFilterMenu({ anchorEl: event.currentTarget, columnKey: column.key });
|
|
setFilterQuery(column.filter.type === "text" ? String(column.filter.value ?? "") : "");
|
|
};
|
|
|
|
const closeColumnFilter = () => {
|
|
setFilterMenu({ anchorEl: null, columnKey: "" });
|
|
setFilterQuery("");
|
|
};
|
|
|
|
const changeFilter = (value) => {
|
|
activeFilter?.onChange?.(value);
|
|
closeColumnFilter();
|
|
};
|
|
|
|
const submitTextFilter = (event) => {
|
|
event.preventDefault();
|
|
activeFilter?.onChange?.(filterQuery.trim());
|
|
closeColumnFilter();
|
|
};
|
|
|
|
const clearTextFilter = () => {
|
|
if (activeFilter?.onReset) {
|
|
activeFilter.onReset();
|
|
} else {
|
|
activeFilter?.onChange?.("");
|
|
}
|
|
closeColumnFilter();
|
|
};
|
|
|
|
const resetColumnFilters = () => {
|
|
closeColumnFilter();
|
|
filterColumns.forEach((column) => {
|
|
if (!isFilterActive(column.filter)) {
|
|
return;
|
|
}
|
|
if (column.filter.onReset) {
|
|
column.filter.onReset();
|
|
return;
|
|
}
|
|
column.filter.onChange?.("");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<section className={["table-frame", className].filter(Boolean).join(" ")}>
|
|
{title || total !== undefined ? (
|
|
<div className="table-toolbar">
|
|
<div className="card-title">{title}</div>
|
|
{total !== undefined ? <div className="muted">共 {total} 条</div> : null}
|
|
</div>
|
|
) : null}
|
|
<div className="table-scroll" ref={scrollRef}>
|
|
<div
|
|
className={["admin-table", resizeSession ? "admin-table--resizing" : "", tableClassName].filter(Boolean).join(" ")}
|
|
style={{ "--admin-table-columns": gridTemplate, "--admin-table-min-width": minWidth }}
|
|
>
|
|
<div className="admin-row admin-row--head">
|
|
{preparedColumns.map((column, index) => {
|
|
const isLastColumn = index === preparedColumns.length - 1;
|
|
return (
|
|
<div
|
|
className={cellClassName(column, ["admin-cell--head", resizeSession?.key === column.key ? "admin-cell--resizing" : ""])}
|
|
data-table-header-cell
|
|
key={column.key}
|
|
>
|
|
<HeaderLabel
|
|
column={column}
|
|
trailingAction={
|
|
isLastColumn && hasColumnFilters && hasActiveColumnFilters ? (
|
|
<TableFilterResetButton onClick={resetColumnFilters} />
|
|
) : null
|
|
}
|
|
onOpenFilter={openColumnFilter}
|
|
/>
|
|
{resizableColumns && column.resizable !== false ? (
|
|
<button
|
|
aria-label={`调整${typeof column.label === "string" ? column.label : column.key}列宽`}
|
|
className="admin-column-resizer"
|
|
tabIndex={-1}
|
|
type="button"
|
|
onPointerDown={(event) => startResize(event, column)}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{safeItems.length ? (
|
|
<>
|
|
{safeItems.map((item, index) => {
|
|
const rowProps = getRowProps ? getRowProps(item, index) : {};
|
|
const { className: rowClassName = "", ...restRowProps } = rowProps;
|
|
|
|
return (
|
|
<div className={["admin-row", rowClassName].filter(Boolean).join(" ")} key={rowKey(item)} {...restRowProps}>
|
|
{preparedColumns.map((column) => (
|
|
<div className={cellClassName(column)} key={column.key}>
|
|
{column.render ? column.render(item, index, renderContext) : displayValue(item[column.key])}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
{infiniteScroll || paginationHasMore || paginationLoadingMore ? (
|
|
<div
|
|
className={["admin-row", "admin-row--load-more", !infiniteHasMore && !infiniteLoading ? "admin-row--load-more-done" : ""]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
ref={loadMoreRef}
|
|
>
|
|
<div className="admin-cell admin-cell--load-more">
|
|
{infiniteLoading ? infiniteLoadLabel : infiniteHasMore ? "" : infiniteDoneLabel}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
) : infiniteLoading ? (
|
|
<div className="admin-row admin-row--load-more" ref={loadMoreRef}>
|
|
<div className="admin-cell admin-cell--load-more">
|
|
{infiniteLoadLabel}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="empty-state empty-state--table">
|
|
<div className="empty-state__title">{emptyLabel}</div>
|
|
</div>
|
|
)}
|
|
{safeItems.length === 0 && (infiniteScroll || paginationHasMore) && !infiniteLoading ? (
|
|
<div className="admin-row--load-more-sentinel" ref={loadMoreRef} />
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
{pagination ? (
|
|
<PaginationBar
|
|
{...pagination}
|
|
itemCount={safeItems.length}
|
|
/>
|
|
) : null}
|
|
<Popover
|
|
anchorEl={filterMenu.anchorEl}
|
|
anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
|
|
className="admin-table-filter-popover"
|
|
open={Boolean(activeFilterColumn)}
|
|
transformOrigin={{ horizontal: "left", vertical: "top" }}
|
|
onClose={closeColumnFilter}
|
|
>
|
|
{activeFilterColumn ? (
|
|
activeFilterIsText ? (
|
|
<form className="admin-table-filter" onSubmit={submitTextFilter}>
|
|
<TextField
|
|
autoFocus
|
|
className="admin-table-filter__search"
|
|
disabled={activeFilter.loading}
|
|
placeholder={activeFilter.placeholder || `搜索${activeFilterColumn.label || ""}`}
|
|
size="small"
|
|
value={filterQuery}
|
|
onChange={(event) => setFilterQuery(event.target.value)}
|
|
/>
|
|
<div className="admin-table-filter__actions">
|
|
<Button disabled={activeFilter.loading || !isFilterActive(activeFilter)} onClick={clearTextFilter}>
|
|
清空
|
|
</Button>
|
|
<Button disabled={activeFilter.loading} type="submit" variant="primary">
|
|
搜索
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
) : (
|
|
<div className="admin-table-filter">
|
|
<TextField
|
|
autoFocus
|
|
className="admin-table-filter__search"
|
|
disabled={activeFilter.loading}
|
|
placeholder={activeFilter.placeholder || `搜索${activeFilterColumn.label || ""}`}
|
|
size="small"
|
|
value={filterQuery}
|
|
onChange={(event) => setFilterQuery(event.target.value)}
|
|
/>
|
|
<MenuList className="admin-table-filter__options">
|
|
{filterOptions.length ? (
|
|
filterOptions.map((option) => (
|
|
<MenuItem
|
|
key={option.value}
|
|
selected={String(activeFilter.value ?? "") === option.value}
|
|
onClick={() => changeFilter(option.value)}
|
|
>
|
|
{option.label}
|
|
</MenuItem>
|
|
))
|
|
) : (
|
|
<div className="admin-table-filter__empty">{activeFilter.loading ? "加载中..." : "无匹配选项"}</div>
|
|
)}
|
|
</MenuList>
|
|
</div>
|
|
)
|
|
) : null}
|
|
</Popover>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function displayValue(value) {
|
|
return value === 0 || value === false || value ? value : "-";
|
|
}
|
|
|
|
function HeaderLabel({ column, onOpenFilter, trailingAction }) {
|
|
const content = column.header || column.label;
|
|
const label = !column.filter ? (
|
|
<span className="admin-cell__head-label">{content}</span>
|
|
) : (
|
|
<FilterHeaderButton column={column} onOpenFilter={onOpenFilter} />
|
|
);
|
|
|
|
return (
|
|
<span className="admin-cell__head-content">
|
|
{label}
|
|
{trailingAction}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function FilterHeaderButton({ column, onOpenFilter }) {
|
|
const content = column.header || column.label;
|
|
if (!column.filter) {
|
|
return <span className="admin-cell__head-label">{content}</span>;
|
|
}
|
|
|
|
const active = isFilterActive(column.filter);
|
|
const filterValueLabel = active ? filterDisplayValue(column.filter) : "";
|
|
return (
|
|
<button
|
|
className={["admin-cell__head-trigger", active ? "admin-cell__head-trigger--active" : ""].filter(Boolean).join(" ")}
|
|
type="button"
|
|
onClick={(event) => onOpenFilter(event, column)}
|
|
>
|
|
<SearchOutlined className="admin-cell__head-icon" fontSize="inherit" />
|
|
<span className="admin-cell__head-label">
|
|
{content}
|
|
{filterValueLabel ? <span className="admin-cell__head-filter">({filterValueLabel})</span> : null}
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function TableFilterResetButton({ onClick }) {
|
|
return (
|
|
<Tooltip arrow title="重置筛选">
|
|
<span className="admin-table-filter-reset-wrap">
|
|
<IconButton
|
|
className="admin-table-filter-reset"
|
|
label="重置筛选"
|
|
sx={tableFilterResetSx}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onClick();
|
|
}}
|
|
>
|
|
<RestartAltOutlined fontSize="small" />
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
function cellClassName(column, extraClassName = "") {
|
|
const extraClassNames = Array.isArray(extraClassName) ? extraClassName : [extraClassName];
|
|
return [
|
|
"admin-cell",
|
|
column.fixed === "left" ? "admin-cell--fixed-left" : "",
|
|
column.fixed === "right" ? "admin-cell--fixed-right" : "",
|
|
column.className || "",
|
|
...extraClassNames
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
}
|
|
|
|
function normalizeColumn(column) {
|
|
const actionColumn = isActionColumn(column);
|
|
const className = [column.className || "", actionColumn ? "admin-cell--actions" : ""].filter(Boolean).join(" ");
|
|
return {
|
|
...column,
|
|
className,
|
|
resizable: actionColumn ? false : column.resizable,
|
|
width: actionColumn ? ACTION_COLUMN_WIDTH : normalizeColumnWidth(column)
|
|
};
|
|
}
|
|
|
|
function normalizeColumnWidth(column) {
|
|
if (!column.width || typeof column.width !== "string") {
|
|
return column.width;
|
|
}
|
|
|
|
const minmaxMatch = column.width.match(/^minmax\(\s*(\d+(?:\.\d+)?)px\s*,\s*(.+)\)$/i);
|
|
if (!minmaxMatch) {
|
|
return column.width;
|
|
}
|
|
|
|
const minWidth = Number(minmaxMatch[1]);
|
|
const flexibleWidth = minmaxMatch[2];
|
|
const maxCompactWidth = isLowPriorityColumn(column) ? LOW_PRIORITY_COMPACT_MIN_WIDTH : DEFAULT_COMPACT_MIN_WIDTH;
|
|
const compactMinWidth = Math.min(minWidth, maxCompactWidth);
|
|
return `minmax(${compactMinWidth}px, ${flexibleWidth})`;
|
|
}
|
|
|
|
function isActionColumn(column) {
|
|
return column.key === "actions" || column.label === "操作";
|
|
}
|
|
|
|
function isLowPriorityColumn(column) {
|
|
return /description|detail|remark|param|url|metadata|context|reason/i.test(String(column.key || ""));
|
|
}
|
|
|
|
function resolveColumnFixed(column, fixedEdges) {
|
|
if (fixedEdges && (column.fixed === "left" || column.fixed === "right")) {
|
|
return column.fixed;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function resolveResizeMinWidth(column) {
|
|
return readPixelWidth(column.resizeMinWidth) || readPixelWidth(column.minWidth) || readMinWidth(column.width) || DEFAULT_RESIZE_MIN_WIDTH;
|
|
}
|
|
|
|
function resolveResizeMaxWidth(column) {
|
|
return readPixelWidth(column.resizeMaxWidth) || readPixelWidth(column.maxWidth) || DEFAULT_RESIZE_MAX_WIDTH;
|
|
}
|
|
|
|
function readMinWidth(width) {
|
|
const pixelWidth = readPixelWidth(width);
|
|
if (pixelWidth) {
|
|
return pixelWidth;
|
|
}
|
|
if (typeof width === "number") {
|
|
return width;
|
|
}
|
|
if (typeof width !== "string") {
|
|
return 0;
|
|
}
|
|
|
|
const minmaxMatch = width.match(/minmax\(\s*(\d+(?:\.\d+)?)px/i);
|
|
if (minmaxMatch) {
|
|
return Number(minmaxMatch[1]);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function readPixelWidth(width) {
|
|
if (typeof width === "number") {
|
|
return width;
|
|
}
|
|
if (typeof width !== "string") {
|
|
return 0;
|
|
}
|
|
const pixelMatch = width.match(/^(\d+(?:\.\d+)?)px$/i);
|
|
return pixelMatch ? Number(pixelMatch[1]) : 0;
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function isFilterActive(filter) {
|
|
const value = filter?.activeValue !== undefined ? filter.activeValue : filter?.value;
|
|
return value !== undefined && value !== null && String(value) !== "";
|
|
}
|
|
|
|
function filterDisplayValue(filter) {
|
|
if (filter?.activeLabel) {
|
|
return filter.activeLabel;
|
|
}
|
|
const value = String((filter?.activeValue !== undefined ? filter.activeValue : filter?.value) ?? "");
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
if (filter?.type === "text") {
|
|
return value;
|
|
}
|
|
|
|
const matchedOption = normalizeFilterOptions(filter).find((option) => option.value === value);
|
|
return matchedOption?.label || value;
|
|
}
|
|
|
|
function filterVisibleOptions(filter, query) {
|
|
if (!filter) {
|
|
return [];
|
|
}
|
|
|
|
const normalizedOptions = normalizeFilterOptions(filter);
|
|
const normalizedQuery = query.trim().toLowerCase();
|
|
if (!normalizedQuery) {
|
|
return normalizedOptions;
|
|
}
|
|
|
|
return normalizedOptions.filter((option) => option.label.toLowerCase().includes(normalizedQuery) || option.value.toLowerCase().includes(normalizedQuery));
|
|
}
|
|
|
|
function normalizeFilterOptions(filter) {
|
|
const options = (filter.options || []).map((option) => {
|
|
if (Array.isArray(option)) {
|
|
return { label: String(option[1] ?? option[0] ?? ""), value: String(option[0] ?? "") };
|
|
}
|
|
return { label: String(option.label ?? option.name ?? option.value ?? ""), value: String(option.value ?? "") };
|
|
});
|
|
|
|
if (filter.emptyLabel !== undefined && !options.some((option) => option.value === "")) {
|
|
return [{ label: filter.emptyLabel, value: "" }, ...options];
|
|
}
|
|
return options;
|
|
}
|
|
|
|
const tableFilterResetSx = {
|
|
width: 28,
|
|
height: 28,
|
|
color: "var(--text-secondary)",
|
|
backgroundColor: "var(--bg-card)",
|
|
"&:hover": {
|
|
borderColor: "var(--primary-border-strong)",
|
|
backgroundColor: "var(--primary-hover)",
|
|
color: "var(--text-primary)"
|
|
}
|
|
};
|