aggrid
This commit is contained in:
331
client/src/components/headers/TextHeaderWithFilter.js
Normal file
331
client/src/components/headers/TextHeaderWithFilter.js
Normal file
@@ -0,0 +1,331 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Typography,
|
||||
IconButton
|
||||
} from '@mui/material';
|
||||
import {
|
||||
FilterList as FilterIcon,
|
||||
ArrowUpward as SortUpIcon,
|
||||
ArrowDownward as SortDownIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
class HeaderComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filterValue: '',
|
||||
sortDirection: null,
|
||||
menuOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Get initial sort direction
|
||||
if (this.props.params && this.props.params.column) {
|
||||
const sort = this.props.params.column.getSort();
|
||||
this.setState({ sortDirection: sort });
|
||||
}
|
||||
|
||||
// Listen for filter changes to update icon color
|
||||
if (this.props.params && this.props.params.api) {
|
||||
this.props.params.api.addEventListener('filterChanged', this.onFilterChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clean up event listener
|
||||
if (this.props.params && this.props.params.api) {
|
||||
this.props.params.api.removeEventListener('filterChanged', this.onFilterChanged);
|
||||
}
|
||||
}
|
||||
|
||||
onFilterChanged = () => {
|
||||
// Force re-render to update filter icon color
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
isFilterActive = () => {
|
||||
if (!this.props.params || !this.props.params.api || !this.props.params.column) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterModel = this.props.params.api.getFilterModel();
|
||||
const colId = this.props.params.column.colId;
|
||||
|
||||
// Check if this column has an active filter
|
||||
return filterModel && filterModel[colId] &&
|
||||
(filterModel[colId].filter !== '' || filterModel[colId].filterType === 'type');
|
||||
};
|
||||
|
||||
onSortRequested = (order, event) => {
|
||||
if (this.props.params && this.props.params.setSort) {
|
||||
this.props.params.setSort(order, event.shiftKey);
|
||||
this.setState({ sortDirection: order });
|
||||
}
|
||||
};
|
||||
|
||||
onFilterIconClick = (event) => {
|
||||
const { params } = this.props;
|
||||
|
||||
try {
|
||||
if (this.state.menuOpen) {
|
||||
// Menu is open, try to close it
|
||||
if (params && params.api) {
|
||||
params.api.hidePopupMenu();
|
||||
}
|
||||
this.setState({ menuOpen: false });
|
||||
} else {
|
||||
// Menu is closed, open it
|
||||
if (params && params.showColumnMenu) {
|
||||
params.showColumnMenu(event.target);
|
||||
this.setState({ menuOpen: true });
|
||||
} else if (params && params.api) {
|
||||
// Alternative method using grid API
|
||||
params.api.showColumnMenuAfterButtonClick(params.column, event.target);
|
||||
this.setState({ menuOpen: true });
|
||||
} else {
|
||||
console.warn('Filter menu not available - params:', params);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling filter menu:', error);
|
||||
this.setState({ menuOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleFilterChange = (event) => {
|
||||
const value = event.target.value;
|
||||
this.setState({ filterValue: value });
|
||||
|
||||
// Apply filter to the column
|
||||
if (this.props.params && this.props.params.api && this.props.params.column) {
|
||||
const colId = this.props.params.column.colId;
|
||||
|
||||
if (value.trim() === '') {
|
||||
// Clear the filter when input is empty
|
||||
const currentFilterModel = this.props.params.api.getFilterModel();
|
||||
delete currentFilterModel[colId];
|
||||
this.props.params.api.setFilterModel(currentFilterModel);
|
||||
} else {
|
||||
// Apply filter when input has value
|
||||
this.props.params.api.setFilterModel({
|
||||
...this.props.params.api.getFilterModel(),
|
||||
[colId]: {
|
||||
type: 'contains',
|
||||
filter: value
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { filterValue, sortDirection } = this.state;
|
||||
const { params } = this.props;
|
||||
|
||||
if (!params) return null;
|
||||
|
||||
const { displayName, column } = params;
|
||||
|
||||
// Only show text input for text columns (Beschreibung, Empfänger/Zahler)
|
||||
const isTextColumn = column.colDef.field === 'description' ||
|
||||
column.colDef.field === 'Beguenstigter/Zahlungspflichtiger';
|
||||
const showTextFilter = isTextColumn;
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderBottom: '1px solid #dee2e6',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
}}>
|
||||
{/* Column Title with proper AG Grid styling */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '13px',
|
||||
color: '#212529',
|
||||
cursor: 'pointer',
|
||||
minWidth: 'fit-content',
|
||||
marginRight: '6px',
|
||||
userSelect: 'none',
|
||||
'&:hover': {
|
||||
color: '#0969da'
|
||||
}
|
||||
}}
|
||||
onClick={(e) => this.onSortRequested(sortDirection === 'asc' ? 'desc' : 'asc', e)}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
|
||||
{/* Sort Icons - Always visible */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginRight: '6px',
|
||||
gap: 0
|
||||
}}>
|
||||
<SortUpIcon sx={{
|
||||
fontSize: '10px',
|
||||
color: sortDirection === 'asc' ? '#0969da' : '#ccc',
|
||||
opacity: sortDirection === 'asc' ? 1 : 0.5,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={(e) => this.onSortRequested('asc', e)}
|
||||
/>
|
||||
<SortDownIcon sx={{
|
||||
fontSize: '10px',
|
||||
color: sortDirection === 'desc' ? '#0969da' : '#ccc',
|
||||
opacity: sortDirection === 'desc' ? 1 : 0.5,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={(e) => this.onSortRequested('desc', e)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Filter Input - only for text columns */}
|
||||
{showTextFilter && (
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder="Filter..."
|
||||
value={filterValue}
|
||||
onChange={this.handleFilterChange}
|
||||
sx={{
|
||||
flex: 1,
|
||||
marginRight: '6px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: '24px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: '#fff',
|
||||
'& fieldset': {
|
||||
borderColor: '#ced4da',
|
||||
borderWidth: '1px'
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#adb5bd'
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0969da',
|
||||
borderWidth: '2px'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: '4px 8px',
|
||||
fontFamily: 'inherit'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Spacer for non-text columns */}
|
||||
{!showTextFilter && <Box sx={{ flex: 1 }} />}
|
||||
|
||||
{/* Filter Menu Icon - Larger hit zone with active state */}
|
||||
<div
|
||||
className="ag-header-menu-button"
|
||||
onClick={this.onFilterIconClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: '24px',
|
||||
minHeight: '24px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: this.isFilterActive() ? '#e3f2fd' : 'transparent'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = this.isFilterActive() ? '#bbdefb' : '#f0f0f0';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = this.isFilterActive() ? '#e3f2fd' : 'transparent';
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="ag-icon ag-icon-filter"
|
||||
role="presentation"
|
||||
unselectable="on"
|
||||
style={{
|
||||
color: this.isFilterActive() ? '#1976d2' : '#666'
|
||||
}}
|
||||
></span>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class TextHeaderWithFilter {
|
||||
constructor() {
|
||||
// Create the DOM element that AG Grid expects
|
||||
this.eGui = document.createElement('div');
|
||||
this.eGui.style.width = '100%';
|
||||
this.eGui.style.height = '100%';
|
||||
this.eGui.style.display = 'flex';
|
||||
this.eGui.style.flexDirection = 'column';
|
||||
|
||||
console.log('TextHeaderWithFilter constructor');
|
||||
}
|
||||
|
||||
init(params) {
|
||||
this.params = params;
|
||||
console.log('TextHeaderWithFilter init params:', params);
|
||||
|
||||
// Listen for menu close events to keep state in sync
|
||||
if (params.api) {
|
||||
params.api.addEventListener('popupMenuVisibleChanged', (event) => {
|
||||
if (!event.visible && this.headerComponent) {
|
||||
this.headerComponent.setState({ menuOpen: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render React component into the DOM element
|
||||
this.renderReactComponent();
|
||||
}
|
||||
|
||||
getGui() {
|
||||
console.log('TextHeaderWithFilter getGui called');
|
||||
return this.eGui;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Use setTimeout to avoid unmounting during render
|
||||
setTimeout(() => {
|
||||
if (this.reactRoot) {
|
||||
this.reactRoot.unmount();
|
||||
this.reactRoot = null;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
renderReactComponent() {
|
||||
// Create React root if it doesn't exist
|
||||
if (!this.reactRoot) {
|
||||
this.reactRoot = createRoot(this.eGui);
|
||||
}
|
||||
|
||||
// Use setTimeout to avoid synchronous render during React render cycle
|
||||
setTimeout(() => {
|
||||
if (this.reactRoot) {
|
||||
this.reactRoot.render(
|
||||
<HeaderComponent
|
||||
ref={(ref) => { this.headerComponent = ref; }}
|
||||
params={this.params}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user