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 (