import { useState, useRef, useEffect } from '@wordpress/element'; import { AgGridReact } from 'ag-grid-react'; import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import LCPDataGridHeader from './LCPDataGridHeader'; import LCPGridColorRender from './LCPGridColorRender'; const LCPDataGrid = ({ dataset, index, updateDataset }) => { const onCellValueChanged = (event) => { // console.log('Cell value changed. Grid index:', index); // Access the grid API const gridApi = gridRef.current.api; // Fetch all the data in the grid after the change const allRowData = []; gridApi.forEachNode(node => allRowData.push(node.data)); // Log the entire row data //console.log('All Grid Data:', allRowData); // Optionally, log the updated row data (just the changed row) // console.log('Updated Row Data:', event.data); // Optionally, log the updated cell value // console.log('Updated Cell Value:', event.newValue); // Create a fresh copy of the dataset and update its data const updatedDataset = { data: allRowData }; console.log(updatedDataset.data); // Send the updated data back to the parent (LCPDatasetBuilder) through updateDataset updateDataset(index, updatedDataset); // Call the parent function to update the dataset }; // lcpCellRenderer to dynamically assign the right cellRenderer function lcpCellRenderer(params) { const value = params.value; // First, check if the value is a Date object if (value instanceof Date) { return value.toLocaleString(); // Return a formatted string (you can format it however you want) } // Check if the value is a valid CSS color (this works for string values like '#232323' or 'red') if (isValidCSSColor(value)) { return ; // Use the color renderer } // Default rendering for other types of data (like text, number, etc.) return value; } const [rowsToAdd, setRowsToAdd] = useState(1); // Number of rows the user wants to add const [currentRowCount, setCurrentRowCount] = useState(dataset.length); // Track the current row count // Button click handler to add a new row const addRows = () => { if (gridApi) { const rows = []; // Create the specified number of new rows (empty rows in this case) for (let i = 0; i < rowsToAdd; i++) { rows.push({}); // You can customize the empty row content if needed } // Use the grid API to add the new rows gridApi.applyTransaction({ add: rows }); // Update the current row count setCurrentRowCount(prevCount => prevCount + rowsToAdd); } else { console.log('Grid is not ready yet'); } }; const gridData = dataset; // Helper function to detect the data type of a value const getDataType = (value) => { if (typeof value === 'number' && !isNaN(value)) { return 'number'; // Identifies numerical values } return 'text'; // Defaults to 'text' for strings and others }; // Helper function to detect if a value is a valid CSS color const isValidCSSColor = (value) => { // Ensure the value is a string before trimming value = String(value).trim(); // Convert to string if it's not already // If the value is a Date object, it should not be processed as a CSS color if (value instanceof Date) { return false; // Return false if it's a Date object } // Regex to match valid CSS color formats (hex, rgb, rgba, hsl, hsla, named colors) const cssColorRegex = /^(#([0-9a-fA-F]{3}){1,2}|[a-zA-Z]+|rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)|rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(0(\.\d+)?|1(\.0+)?)\)|hsl\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)|hsla\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,\s*(0(\.\d+)?|1(\.0+)?)\))$/i; // First check if the value matches the general color regex if (!cssColorRegex.test(value)) { return false; // If it doesn't match any known color format, it's not valid } // Now, we'll do more detailed checks for RGB, RGBA, HSL, and HSLA formats // Check if it's a valid RGB format (rgb(r, g, b)) const rgbMatch = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i.exec(value); if (rgbMatch) { const r = parseInt(rgbMatch[1], 10); const g = parseInt(rgbMatch[2], 10); const b = parseInt(rgbMatch[3], 10); // RGB values should be in the range 0-255 return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255; } // Check if it's a valid RGBA format (rgba(r, g, b, a)) const rgbaMatch = /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(0(\.\d+)?|1(\.0+)?)\)$/i.exec(value); if (rgbaMatch) { const r = parseInt(rgbaMatch[1], 10); const g = parseInt(rgbaMatch[2], 10); const b = parseInt(rgbaMatch[3], 10); const a = parseFloat(rgbaMatch[4]); // RGBA values should be in the range 0-255 for RGB and 0-1 for alpha return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255 && a >= 0 && a <= 1; } // Check if it's a valid HSL format (hsl(h, s%, l%)) const hslMatch = /^hsl\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)$/i.exec(value); if (hslMatch) { const h = parseInt(hslMatch[1], 10); const s = parseInt(hslMatch[2], 10); const l = parseInt(hslMatch[3], 10); // Hue should be 0-360, saturation and lightness should be 0-100 return h >= 0 && h <= 360 && s >= 0 && s <= 100 && l >= 0 && l <= 100; } // Check if it's a valid HSLA format (hsla(h, s%, l%, a)) const hslaMatch = /^hsla\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,\s*(0(\.\d+)?|1(\.0+)?)\)$/i.exec(value); if (hslaMatch) { const h = parseInt(hslaMatch[1], 10); const s = parseInt(hslaMatch[2], 10); const l = parseInt(hslaMatch[3], 10); const a = parseFloat(hslaMatch[4]); // Hue should be 0-360, saturation and lightness should be 0-100, alpha should be 0-1 return h >= 0 && h <= 360 && s >= 0 && s <= 100 && l >= 0 && l <= 100 && a >= 0 && a <= 1; } // If it's a valid named color (like "red", "blue", etc.) const namedColors = [ "red", "green", "blue", "yellow", "black", "white", "gray", "purple", "orange", "brown", "pink", "cyan", "magenta", // Add more CSS named colors as necessary ]; if (namedColors.includes(value.toLowerCase())) { return true; // It's a valid named color } // If it's a valid hex color (hex format should be valid) const hexMatch = /^#([0-9a-fA-F]{3}){1,2}$/i.exec(value); if (hexMatch) { return true; // It's a valid hex color } // If it passed regex but didn't pass the detailed checks, it's invalid return false; }; // Helper function to detect if a value contains HTML tags const isHTML = (value) => { const htmlRegex = /<([a-z][\s\S]*)>/i; // A simple check for any HTML tag return htmlRegex.test(value); }; // Helper function to detect if a value is a valid date const isDate = (value) => { const date = new Date(value); return !isNaN(date.getTime()); // Return true if it's a valid date }; // Detect the data type of each key by checking all rows const detectDataTypes = (data) => { const keys = Object.keys(data[0]); let dataTypes = {}; keys.forEach((key) => { // Check all values for the key across all rows const allValuesAreNumbers = data.every(row => !isNaN(Number(row[key]))); const allValuesAreColors = data.every(row => isValidCSSColor(row[key])); const allValuesAreHTML = data.every(row => isHTML(row[key])); const allValuesAreDates = data.every(row => isDate(row[key])); // If all values for that key are valid numbers, mark it as 'number' if (allValuesAreNumbers) { dataTypes[key] = 'number'; } // If all values for that key are valid CSS colors, mark it as 'color' else if (allValuesAreColors) { dataTypes[key] = 'color'; } // If all values for that key contain HTML, mark it as 'html' else if (allValuesAreHTML) { dataTypes[key] = 'html'; } // If all values for that key are valid dates, mark it as 'date' else if (allValuesAreDates) { dataTypes[key] = 'date'; } // If neither, mark it as 'text' else { dataTypes[key] = 'text'; } }); return dataTypes; }; // Use useEffect to update dataTypes whenever data changes useEffect(() => { if (data && data.length > 0) { const detectedDataTypes = detectDataTypes(data); setAttributes({ dataTypes: detectedDataTypes }); } }, [data, setAttributes]); // Assuming gridData is already populated const gridDataTypes = detectDataTypes(gridData); // This will detect the types of each field console.log(gridDataTypes); // For debugging, will output the detected types for each column // Helper function to convert index to letters, accounting for 'A1', 'B1', 'C1', etc. const getColumnLabel = (index) => { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; let label = ''; while (index >= 0) { label = alphabet[index % 26] + label; index = Math.floor(index / 26) - 1; } return label; }; function lcpDataTypeParser(params) { const columnName = params.colDef.field; // Get the column field name (e.g., 'Budget', 'MeetAt') const dataType = gridDataTypes[columnName]; // Get the data type for this column from gridDataTypes if (params.node.rowPinned) { // If it's the pinned top row, return the value as-is return params.newValue; } else { // Check if the value is a Date object first let value = params.newValue; if (value instanceof Date) { // If it's a Date object, return the string representation (this could be customized if needed) return value.toLocaleString(); // or use `toISOString()` if you prefer } // Process based on the detected data type switch (dataType) { case 'number': // If it's a number, return the number or NaN return Number(value); case 'date': const dateStr = value; // Check if the value is an empty string or undefined if (!dateStr || dateStr.trim() === '') { return null; // If empty, return null or any default value you prefer } let parsedDate; // Check if the date is in the format "MM/DD/YYYY" const isValidMMDDYYYY = /^\d{2}\/\d{2}\/\d{4}$/.test(dateStr); if (isValidMMDDYYYY) { const [month, day, year] = dateStr.split('/'); const isoDateStr = `${year}-${month}-${day}`; parsedDate = new Date(isoDateStr); } else { // For other formats, try using the default Date parser parsedDate = new Date(dateStr); } // Return parsed date if valid, otherwise return the original string return !isNaN(parsedDate.getTime()) ? parsedDate : dateStr; case 'color': // If it's a color, return the value as-is return value; case 'html': // If it's HTML, we can either sanitize or leave the value as is (for now, leaving it as is) return value; default: // If it's text (or any other type), return the value as-is return value; } } } // Create columnDefs dynamically const [columnDefs, setColumnDefs] = useState([]); const [pinnedTopRowData, setPinnedTopRowData] = useState([]); useEffect(() => { if (columnDefs.length > 0) return; // Prevent rerun if columnDefs already set if (gridData.length > 0) { const keys = Object.keys(gridData[0]); // Get the keys from the first row (Department2, Budget, etc.) const columns = keys.map((key, index) => { return { colId: getColumnLabel(index), // 'A', 'B', 'C', ... headerName: getColumnLabel(index), // 'A', 'B', 'C', ... field: key, // Field will match the key (Department2, Budget, etc.) valueParser: lcpDataTypeParser, cellRenderer: lcpCellRenderer, // Reference the custom cell renderer headerComponent: LCPDataGridHeader, lcpDataType: 'myCustomDataType' // Add your custom parameter here }; }); // Set the pinned top row data with the actual field names (like 'Department2', 'Budget') const topRowData = keys.reduce((acc, key) => { acc[key] = key; // Set key as the value in top row (use field names) return acc; }, {}); // Ensure pinnedTopRowData is set with correct data setPinnedTopRowData([topRowData]); // Add the row number column as the first column const rowNumberColumn = { headerName: '', valueGetter: (params) => { const rowIndex = params.node.rowPinned ? params.node.rowIndex + 1 : params.node.rowIndex + 2; return rowIndex; }, suppressMovable: true, editable: false, filter: false, sortable: false, width: 45, resizable: false, flex: 0, cellStyle: { backgroundColor: '#e0e0e0' }, }; setColumnDefs([rowNumberColumn, ...columns]); // Set columns along with row number column } }, [gridData, columnDefs]); // R const [rowData] = useState(gridData); // AG Grid instance reference const gridRef = useRef(null); const [gridApi, setGridApi] = useState(null); // Store grid API in state // Default Column Properties const defaultColDef = { flex: 1, minWidth: 45, editable: true, sortable: true, filter: true, suppressMovable: true, cellDataType: false, //By default column can be any data type cellStyle: (params) => { if (params.node.rowPinned) { return { backgroundColor: 'rgb(197, 219, 229)' }; } } }; const gridOptions = { rowDragManaged: true, animateRows: true, rowHeight: 35, getRowStyle: (params) => { if (params.node.rowPinned) { return { backgroundColor: 'rgb(197, 219, 229)' }; // Light gray for pinned rows } return null; // No special style for non-pinned rows } }; const onGridReady = (params) => { // Store the grid API when the grid is ready setGridApi(params.api); }; // Reset row number on sorted const onSortChanged = (e) => { e.api.refreshCells(); }; // Reset row number on filtered const onFilterChanged = (e) => { e.api.refreshCells(); }; return (
{/* Grid Footer */}
{/* Input to add multiple rows */}
setRowsToAdd(Math.max(1, parseInt(e.target.value, 10)))} min="1" />
{currentRowCount} Rows
); }; export default LCPDataGrid;