UNPKG

@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
"use strict"; 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" })] })] })) })); }