458 lines
17 KiB
JavaScript
458 lines
17 KiB
JavaScript
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 <LCPGridColorRender value={value} />; // 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 (
|
|
<div>
|
|
<div
|
|
id="lcp-data-grid"
|
|
className="ag-theme-alpine"
|
|
style={{ width: '100%', height: '300px' }}
|
|
>
|
|
<AgGridReact
|
|
ref={gridRef}
|
|
columnDefs={columnDefs}
|
|
rowData={rowData}
|
|
defaultColDef={defaultColDef}
|
|
gridOptions={gridOptions}
|
|
pinnedTopRowData={pinnedTopRowData}
|
|
pinnedLeftColCount={1}
|
|
onSortChanged={onSortChanged}
|
|
onFilterChanged={onFilterChanged}
|
|
onGridReady={onGridReady}
|
|
onCellValueChanged={onCellValueChanged}
|
|
|
|
/>
|
|
</div>
|
|
|
|
{/* Grid Footer */}
|
|
<div style={{padding:'15px 0px',display:'flex', flexDirection:'row', justifyContent:'space-between', background:'white'}}>
|
|
{/* Input to add multiple rows */}
|
|
<div style={{display:'flex', flexDirection:'row'}}>
|
|
<input
|
|
type="number"
|
|
value={rowsToAdd}
|
|
onChange={(e) => setRowsToAdd(Math.max(1, parseInt(e.target.value, 10)))}
|
|
min="1"
|
|
/>
|
|
<button onClick={addRows}>Add Row(s)</button>
|
|
</div>
|
|
<div>
|
|
<span>{currentRowCount} Rows</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LCPDataGrid;
|