import { useMemo, useCallback, useRef, useState } from '@wordpress/element'; import { Popover, Button, Modal, SelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { AgGridReact } from 'ag-grid-react'; import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; const generateId = () => { const randomStr = Math.random().toString(36).substring(2, 8); return `lcpDatapoint-${randomStr}`; }; // Helper function to determine text color based on background const getContrastColor = (hexcolor) => { if (!hexcolor) return 'inherit'; const r = parseInt(hexcolor.slice(1, 3), 16); const g = parseInt(hexcolor.slice(3, 5), 16); const b = parseInt(hexcolor.slice(5, 7), 16); const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000; return (yiq >= 128) ? 'black' : 'white'; }; const ColorCellRenderer = (props) => { if (!props.value) return ''; return (
{props.value}
); }; const ParentCellRenderer = (props) => { if (!props.data || !props.data.ID) { return null; } // Create options from all available rows except current const options = [ { label: __('None', 'lcp-visualize'), value: '' } ]; // Get all rows from the grid props.api.forEachNode(node => { if (node.data && node.data.ID && node.data.ID !== props.data.ID) { options.push({ label: node.data.Label || node.data.ID, value: node.data.ID }); } }); const handleChange = (newValue) => { if (props.context && props.context.handleParentChange) { props.context.handleParentChange(props.data, newValue); } }; return ( ); }; const ColumnHeaderComponent = (props) => { const [isModalOpen, setIsModalOpen] = useState(false); const [headerName, setHeaderName] = useState(props.displayName); const dataTypeOptions = [ { label: __('Text', 'lcp'), value: 'lcpText' }, { label: __('Number', 'lcp'), value: 'lcpNumber' }, { label: __('Color', 'lcp'), value: 'lcpColor' }, { label: __('Date/Time', 'lcp'), value: 'lcpDate' } ]; const handleTypeChange = (newType) => { props.setColumnType(props.column.colId, newType); }; const handleHeaderChange = (newHeader) => { if (props.onHeaderChange && newHeader !== props.displayName) { props.onHeaderChange(props.column.colId, newHeader); } }; const handleSave = () => { handleHeaderChange(headerName); setIsModalOpen(false); }; const handleCancel = () => { setHeaderName(props.displayName); setIsModalOpen(false); }; return (
{props.displayName}
)} ); }; const LCPDataGrid = ({ chartData = [], onDataChange, columnTypes, onColumnTypeChange, attributes, setAttributes }) => { const gridRef = useRef(); const [contextMenu, setContextMenu] = useState(null); // Helper function to guess column type based on value const guessColumnType = useCallback((value) => { if (typeof value === 'number') return 'lcpNumber'; if (typeof value === 'string') { if (/^#([0-9A-F]{3}){1,2}$/i.test(value)) return 'lcpColor'; if (!isNaN(Date.parse(value))) return 'lcpDate'; } return 'lcpText'; }, []); // Handle parent change const handleParentChange = useCallback((data, newParent) => { const newData = chartData.map(item => item.ID === data.ID ? { ...item, Parent: newParent } : item ); onDataChange(newData); }, [chartData, onDataChange]); // Use columnTypes from attributes if available, otherwise use direct columnTypes prop const effectiveColumnTypes = attributes?.columnTypes || columnTypes || {}; // Convert any old column types to new format const migratedColumnTypes = useMemo(() => { const typeMap = { 'textColumn': 'lcpText', 'numericColumn': 'lcpNumber', 'colorColumn': 'lcpColor', 'dateColumn': 'lcpDate' }; return Object.entries(effectiveColumnTypes).reduce((acc, [key, value]) => { acc[key] = typeMap[value] || value; return acc; }, {}); }, [effectiveColumnTypes]); const handleColumnTypeChange = useCallback((colId, newType) => { if (setAttributes) { setAttributes({ columnTypes: { ...migratedColumnTypes, [colId]: newType } }); } else if (onColumnTypeChange) { onColumnTypeChange(colId, newType); } }, [migratedColumnTypes, setAttributes, onColumnTypeChange]); // Define column type configurations const gridColumnTypes = useMemo(() => ({ lcpNumber: { filter: 'agNumberColumnFilter', filterParams: { buttons: ['apply', 'reset'], closeOnApply: true }, valueParser: params => { if (!params.newValue) return ''; const parsed = parseFloat(params.newValue); return isNaN(parsed) ? '' : parsed; } }, lcpText: { filter: 'agTextColumnFilter', filterParams: { buttons: ['apply', 'reset'], closeOnApply: true } }, lcpColor: { filter: 'agTextColumnFilter', filterParams: { buttons: ['apply', 'reset'], closeOnApply: true }, cellRenderer: ColorCellRenderer, valueParser: params => { if (!params.newValue) return ''; return /^#([0-9A-F]{3}){1,2}$/i.test(params.newValue) ? params.newValue.toUpperCase() : ''; } }, lcpDate: { filter: 'agDateColumnFilter', filterParams: { buttons: ['apply', 'reset'], closeOnApply: true }, valueParser: params => { if (!params.newValue) return ''; const date = new Date(params.newValue); return isNaN(date.getTime()) ? '' : date.toISOString(); } } }), []); const handleHeaderChange = useCallback((oldHeader, newHeader) => { if (oldHeader === newHeader || !chartData.length) return; // Update the chartData with the new header const newData = chartData.map(row => { const { [oldHeader]: value, ...rest } = row; return { ...rest, [newHeader]: value }; }); onDataChange(newData); }, [chartData, onDataChange]); // Generate column definitions based on chartData const columnDefs = useMemo(() => { if (!chartData.length) return []; // Get all unique keys from the data const allKeys = Array.from(new Set( chartData.flatMap(item => Object.keys(item)) )).filter(key => key !== 'Parent'); // Remove Parent from regular columns // Create column definitions for regular columns const regularColumns = allKeys.map(key => { const sampleValue = chartData.find(item => item[key] !== undefined)?.[key]; const defaultType = guessColumnType(sampleValue); const currentType = migratedColumnTypes[key] || defaultType; return { field: key, headerName: key, hide: key === 'ID', headerComponent: ColumnHeaderComponent, headerComponentParams: { displayName: key, onHeaderChange: handleHeaderChange, setColumnType: handleColumnTypeChange, currentType: currentType }, type: currentType, editable: key !== 'ID', sortable: true, filter: true }; }); // Add Parent column with ParentCellRenderer const parentColumn = { field: 'Parent', headerName: __('Parent', 'lcp-visualize'), cellRenderer: ParentCellRenderer, cellRendererParams: { context: { handleParentChange } }, editable: false, sortable: true, filter: true, minWidth: 200 }; return [...regularColumns, parentColumn]; }, [chartData, migratedColumnTypes, handleColumnTypeChange, handleHeaderChange, guessColumnType, handleParentChange]); const defaultColDef = useMemo(() => ({ flex: 1, minWidth: 100, editable: true, sortable: true, filter: true, resizable: true }), []); const rowData = useMemo(() => { return chartData.map(point => ({ ...point, ID: point.ID || generateId() })); }, [chartData]); const handleContextMenu = useCallback((event) => { event.preventDefault(); const target = event.target; const rowCell = target.closest('.ag-cell'); if (!rowCell) return; const rect = rowCell.getBoundingClientRect(); const rowNode = gridRef.current.api.getRowNode(rowCell.parentElement.getAttribute('row-index')); if (!rowNode || !rowNode.data || !rowNode.data.ID) return; setContextMenu({ type: 'row', position: { top: rect.bottom, left: rect.left }, rowIndex: rowNode.rowIndex, totalRows: gridRef.current.api.getDisplayedRowCount() }); }, []); const handleRowAction = useCallback((action, rowIndex) => { const newData = [...chartData]; // Create empty row with all existing columns const emptyRow = { ID: generateId() }; if (chartData.length > 0) { // Get all columns from existing data const columns = Array.from(new Set( chartData.flatMap(item => Object.keys(item)) )); // Initialize each column with an appropriate empty value columns.forEach(col => { if (col === 'ID') return; // Skip Id as it's already set // Get a sample value to determine appropriate empty value const sampleValue = chartData.find(item => item[col] !== undefined)?.[col]; switch (typeof sampleValue) { case 'number': emptyRow[col] = 0; break; case 'boolean': emptyRow[col] = false; break; default: emptyRow[col] = ''; } }); } switch (action) { case 'add-above': newData.splice(rowIndex - 1, 0, emptyRow); break; case 'add-below': newData.splice(rowIndex, 0, emptyRow); break; case 'delete': newData.splice(rowIndex - 1, 1); break; } onDataChange(newData); setContextMenu(null); }, [chartData, onDataChange]); const handleCellValueChanged = useCallback((params) => { if (!params.data || !params.data.ID) return; const newData = chartData.map(item => item.ID === params.data.ID ? params.data : item ); onDataChange(newData); }, [chartData, onDataChange]); return (
{contextMenu && ( setContextMenu(null)} anchorRect={{ top: contextMenu.mouseEvent.clientY, left: contextMenu.mouseEvent.clientX, width: 0, height: 0, }} >
)}
); }; export default LCPDataGrid;