@ackplus/react-tanstack-data-table
Version:
A powerful React data table component built with MUI and TanStack Table
208 lines (207 loc) • 15.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ColumnFilterControl = ColumnFilterControl;
const jsx_runtime_1 = require("react/jsx-runtime");
const icons_material_1 = require("@mui/icons-material");
const material_1 = require("@mui/material");
const react_1 = __importStar(require("react"));
const menu_dropdown_1 = require("../droupdown/menu-dropdown");
const data_table_context_1 = require("../../contexts/data-table-context");
const icons_1 = require("../../icons");
const column_helpers_1 = require("../../utils/column-helpers");
const slot_helpers_1 = require("../../utils/slot-helpers");
const filters_1 = require("../filters");
const filter_value_input_1 = require("../filters/filter-value-input");
function ColumnFilterControl(props = {}) {
var _a, _b, _c;
const { table, slots, slotProps } = (0, data_table_context_1.useDataTableContext)();
// Extract slot-specific props with enhanced merging
const iconSlotProps = (0, slot_helpers_1.extractSlotProps)(slotProps, 'filterIcon');
const FilterIconSlot = (0, slot_helpers_1.getSlotComponent)(slots, 'filterIcon', icons_material_1.FilterList);
// Use the custom feature state from the table - now using pending filters for UI
const filterState = ((_a = table === null || table === void 0 ? void 0 : table.getColumnFilterState) === null || _a === void 0 ? void 0 : _a.call(table)) || {
filters: [],
logic: 'AND',
pendingFilters: [],
pendingLogic: 'AND'
};
// Use pending filters for the UI (draft state)
const filters = filterState.pendingFilters;
const filterLogic = filterState.pendingLogic;
// Active filters are the actual applied filters
const activeFiltersCount = ((_c = (_b = table === null || table === void 0 ? void 0 : table.getActiveColumnFilters) === null || _b === void 0 ? void 0 : _b.call(table)) === null || _c === void 0 ? void 0 : _c.length) || 0;
const filterableColumns = (0, react_1.useMemo)(() => {
return table === null || table === void 0 ? void 0 : table.getAllLeafColumns().filter(column => (0, column_helpers_1.isColumnFilterable)(column));
}, [table]);
const addFilter = (0, react_1.useCallback)((columnId, operator) => {
var _a, _b;
// If no column specified, use empty (user will select)
// If column specified, get its appropriate default operator
let defaultOperator = operator || '';
if (columnId && !operator) {
const column = filterableColumns === null || filterableColumns === void 0 ? void 0 : filterableColumns.find(col => col.id === columnId);
const columnType = (0, column_helpers_1.getColumnType)(column);
const operators = filters_1.FILTER_OPERATORS[columnType] || filters_1.FILTER_OPERATORS.text;
defaultOperator = ((_a = operators[0]) === null || _a === void 0 ? void 0 : _a.value) || 'contains';
}
(_b = table === null || table === void 0 ? void 0 : table.addPendingColumnFilter) === null || _b === void 0 ? void 0 : _b.call(table, columnId || '', defaultOperator, '');
}, [table, filterableColumns]);
const handleAddFilter = (0, react_1.useCallback)(() => {
addFilter();
}, [addFilter]);
const updateFilter = (0, react_1.useCallback)((filterId, updates) => {
var _a;
(_a = table === null || table === void 0 ? void 0 : table.updatePendingColumnFilter) === null || _a === void 0 ? void 0 : _a.call(table, filterId, updates);
}, [table]);
const removeFilter = (0, react_1.useCallback)((filterId) => {
var _a;
(_a = table === null || table === void 0 ? void 0 : table.removePendingColumnFilter) === null || _a === void 0 ? void 0 : _a.call(table, filterId);
}, [table]);
const clearAllFilters = (0, react_1.useCallback)((closeDialog) => {
var _a;
// Clear all pending filters
(_a = table === null || table === void 0 ? void 0 : table.clearAllPendingColumnFilters) === null || _a === void 0 ? void 0 : _a.call(table);
// Immediately apply the clear (which will clear active filters too)
setTimeout(() => {
var _a;
(_a = table === null || table === void 0 ? void 0 : table.applyPendingColumnFilters) === null || _a === void 0 ? void 0 : _a.call(table);
// Close dialog if callback provided
if (closeDialog) {
closeDialog();
}
}, 0);
}, [table]);
// Handle filter logic change (AND/OR)
const handleLogicChange = (0, react_1.useCallback)((newLogic) => {
var _a;
(_a = table === null || table === void 0 ? void 0 : table.setPendingFilterLogic) === null || _a === void 0 ? void 0 : _a.call(table, newLogic);
}, [table]);
// Apply all pending filters
const applyFilters = (0, react_1.useCallback)(() => {
var _a;
(_a = table === null || table === void 0 ? void 0 : table.applyPendingColumnFilters) === null || _a === void 0 ? void 0 : _a.call(table);
}, [table]);
// Handle apply button click
const handleApplyFilters = (0, react_1.useCallback)((closeDialog) => {
applyFilters();
closeDialog();
}, [applyFilters]);
const getOperatorsForColumn = (0, react_1.useCallback)((columnId) => {
const column = filterableColumns === null || filterableColumns === void 0 ? void 0 : filterableColumns.find(col => col.id === columnId);
const type = (0, column_helpers_1.getColumnType)(column);
return filters_1.FILTER_OPERATORS[type] || filters_1.FILTER_OPERATORS.text;
}, [filterableColumns]);
// Handle column selection change
const handleColumnChange = (0, react_1.useCallback)((filterId, newColumnId, currentFilter) => {
var _a;
const newColumn = filterableColumns === null || filterableColumns === void 0 ? void 0 : filterableColumns.find(col => col.id === newColumnId);
const columnType = (0, column_helpers_1.getColumnType)(newColumn);
const operators = filters_1.FILTER_OPERATORS[columnType] || filters_1.FILTER_OPERATORS.text;
// Only reset operator if current operator is not valid for new column type
const currentOperatorValid = operators.some(op => op.value === currentFilter.operator);
const newOperator = currentOperatorValid ? currentFilter.operator : ((_a = operators[0]) === null || _a === void 0 ? void 0 : _a.value) || '';
updateFilter(filterId, {
columnId: newColumnId,
operator: newOperator,
// Keep the current value unless operator is empty/notEmpty
value: ['isEmpty', 'isNotEmpty'].includes(newOperator) ? '' : currentFilter.value,
});
}, [filterableColumns, updateFilter]);
// Handle operator selection change
const handleOperatorChange = (0, react_1.useCallback)((filterId, newOperator, currentFilter) => {
updateFilter(filterId, {
operator: newOperator,
// Only reset value if operator is empty/notEmpty, otherwise preserve it
value: ['isEmpty', 'isNotEmpty'].includes(newOperator) ? '' : currentFilter.value,
});
}, [updateFilter]);
// Handle filter value change
const handleFilterValueChange = (0, react_1.useCallback)((filterId, value) => {
updateFilter(filterId, { value });
}, [updateFilter]);
// Handle filter removal
const handleRemoveFilter = (0, react_1.useCallback)((filterId) => {
removeFilter(filterId);
}, [removeFilter]);
// Count pending filters that are ready to apply (have column, operator, and value OR are empty/notEmpty operators)
const pendingFiltersCount = filters.filter(f => {
if (!f.columnId || !f.operator)
return false;
// For empty/notEmpty operators, no value is needed
if (['isEmpty', 'isNotEmpty'].includes(f.operator))
return true;
// For other operators, value is required
return f.value && f.value.toString().trim() !== '';
}).length;
// Check if we need to show "Clear Applied Filters" button
const hasAppliedFilters = activeFiltersCount > 0;
// Determine if there are pending changes that can be applied
const hasPendingChanges = pendingFiltersCount > 0 || (filters.length === 0 && hasAppliedFilters);
// Auto-add default filter when opening if no filters exist AND no applied filters
(0, react_1.useEffect)(() => {
var _a;
if (filters.length === 0 && filterableColumns && (filterableColumns === null || filterableColumns === void 0 ? void 0 : filterableColumns.length) > 0 && activeFiltersCount === 0) {
const firstColumn = filterableColumns[0];
const columnType = (0, column_helpers_1.getColumnType)(firstColumn);
const operators = filters_1.FILTER_OPERATORS[columnType] || filters_1.FILTER_OPERATORS.text;
const defaultOperator = ((_a = operators[0]) === null || _a === void 0 ? void 0 : _a.value) || 'contains';
// Add default filter with first column and its first operator
addFilter(firstColumn === null || firstColumn === void 0 ? void 0 : firstColumn.id, defaultOperator);
}
}, [filters.length, filterableColumns, addFilter, activeFiltersCount]);
// Merge all props for maximum flexibility
const mergedProps = (0, slot_helpers_1.mergeSlotProps)({
// Default props
size: 'small',
sx: { flexShrink: 0 },
}, (slotProps === null || slotProps === void 0 ? void 0 : slotProps.columnFilterControl) || {}, props);
return ((0, jsx_runtime_1.jsx)(menu_dropdown_1.MenuDropdown, { anchor: ((0, jsx_runtime_1.jsx)(material_1.Badge, { badgeContent: activeFiltersCount > 0 ? activeFiltersCount : 0, color: "primary", invisible: activeFiltersCount === 0, ...mergedProps.badgeProps, children: (0, jsx_runtime_1.jsx)(material_1.IconButton, { ...mergedProps, children: (0, jsx_runtime_1.jsx)(FilterIconSlot, { ...iconSlotProps }) }) })), children: ({ handleClose }) => ((0, jsx_runtime_1.jsxs)(material_1.Box, { sx: {
p: 2,
minWidth: 400,
maxWidth: 600,
...mergedProps.menuSx,
}, children: [(0, jsx_runtime_1.jsx)(material_1.Typography, { variant: "subtitle2", sx: {
mb: 1,
...mergedProps.titleSx,
}, children: mergedProps.title || 'Column Filters' }), (0, jsx_runtime_1.jsx)(material_1.Divider, { sx: { mb: 2 } }), filters.length > 1 && ((0, jsx_runtime_1.jsx)(material_1.Box, { sx: { mb: 2 }, children: (0, jsx_runtime_1.jsxs)(material_1.FormControl, { size: "small", sx: { minWidth: 120 }, children: [(0, jsx_runtime_1.jsx)(material_1.InputLabel, { children: "Logic" }), (0, jsx_runtime_1.jsxs)(material_1.Select, { value: filterLogic, label: "Logic", onChange: (e) => handleLogicChange(e.target.value), ...mergedProps.logicSelectProps, children: [(0, jsx_runtime_1.jsx)(material_1.MenuItem, { value: "AND", children: "AND" }), (0, jsx_runtime_1.jsx)(material_1.MenuItem, { value: "OR", children: "OR" })] })] }) })), (0, jsx_runtime_1.jsx)(material_1.Stack, { spacing: 2, sx: { mb: 2 }, children: filters.map((filter) => {
const selectedColumn = filterableColumns === null || filterableColumns === void 0 ? void 0 : filterableColumns.find(col => col.id === filter.columnId);
const operators = filter.columnId ? getOperatorsForColumn(filter.columnId) : [];
const needsValue = !['isEmpty', 'isNotEmpty'].includes(filter.operator);
return ((0, jsx_runtime_1.jsxs)(material_1.Stack, { direction: "row", spacing: 1, alignItems: "center", children: [(0, jsx_runtime_1.jsxs)(material_1.FormControl, { size: "small", sx: { minWidth: 120 }, children: [(0, jsx_runtime_1.jsx)(material_1.InputLabel, { children: "Column" }), (0, jsx_runtime_1.jsx)(material_1.Select, { value: filter.columnId || '', label: "Column", onChange: (e) => handleColumnChange(filter.id, e.target.value, filter), children: filterableColumns === null || filterableColumns === void 0 ? void 0 : filterableColumns.map(column => ((0, jsx_runtime_1.jsx)(material_1.MenuItem, { value: column.id, children: typeof column.columnDef.header === 'string'
? column.columnDef.header
: column.id }, column.id))) })] }), (0, jsx_runtime_1.jsxs)(material_1.FormControl, { size: "small", sx: { minWidth: 120 }, children: [(0, jsx_runtime_1.jsx)(material_1.InputLabel, { children: "Operator" }), (0, jsx_runtime_1.jsx)(material_1.Select, { value: filter.operator || '', label: "Operator", onChange: (e) => handleOperatorChange(filter.id, e.target.value, filter), disabled: !filter.columnId, children: operators.map(op => ((0, jsx_runtime_1.jsx)(material_1.MenuItem, { value: op.value, children: op.label }, op.value))) })] }), needsValue && selectedColumn && ((0, jsx_runtime_1.jsx)(filter_value_input_1.FilterValueInput, { filter: filter, column: selectedColumn, onValueChange: (value) => handleFilterValueChange(filter.id, value) })), (0, jsx_runtime_1.jsx)(material_1.IconButton, { size: "small", onClick: () => handleRemoveFilter(filter.id), color: "error", ...mergedProps.deleteButtonProps, children: (0, jsx_runtime_1.jsx)(icons_1.DeleteIcon, { fontSize: "small" }) })] }, filter.id));
}) }), (0, jsx_runtime_1.jsx)(material_1.Button, { variant: "outlined", size: "small", startIcon: (0, jsx_runtime_1.jsx)(icons_1.AddIcon, {}), onClick: handleAddFilter, disabled: !filterableColumns || filterableColumns.length === 0, sx: { mb: 2 }, ...mergedProps.addButtonProps, children: "Add Filter" }), (0, jsx_runtime_1.jsxs)(material_1.Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", children: [hasAppliedFilters && ((0, jsx_runtime_1.jsx)(material_1.Button, { variant: "outlined", size: "small", onClick: () => clearAllFilters(handleClose), color: "error", ...mergedProps.clearButtonProps, children: "Clear All" })), (0, jsx_runtime_1.jsx)(material_1.Button, { variant: "contained", size: "small", onClick: () => handleApplyFilters(handleClose), disabled: !hasPendingChanges, ...mergedProps.applyButtonProps, children: "Apply" })] })] })) }));
}