feat: reusable ag grid

This commit is contained in:
2025-09-03 17:28:04 +03:30
parent dafb07375a
commit c382d97d8d
7 changed files with 1311 additions and 560 deletions

View File

@@ -5,15 +5,23 @@ export function useDataTableActions(gridApi, rowData) {
const showDeleteDialog = ref(false)
const selectedRowData = ref(null)
const deleteRowData = ref(null)
const selectedRowNodeId = ref(null)
const deleteRowNodeId = ref(null)
const getRowDataByIndex = (rowIndex) => {
const getRowDataByIndex = (rowIndex, nodeId) => {
if (!gridApi.value) {
console.error('Grid API not available')
return null
}
try {
const node = gridApi.value.getDisplayedRowAtIndex(rowIndex)
let node = null
if (nodeId != null && gridApi.value.getRowNode) {
node = gridApi.value.getRowNode(nodeId)
}
if (!node) {
node = gridApi.value.getDisplayedRowAtIndex(rowIndex)
}
if (node && node.data) {
return node.data
}
@@ -33,9 +41,10 @@ export function useDataTableActions(gridApi, rowData) {
const showDetails = (rowIndex, nodeId) => {
console.log('Show details called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex)
if (rowData) {
selectedRowData.value = { ...rowData }
const rowDataItem = getRowDataByIndex(rowIndex, nodeId)
if (rowDataItem) {
selectedRowData.value = { ...rowDataItem }
selectedRowNodeId.value = nodeId
showDetailsDialog.value = true
console.log('Selected row data:', selectedRowData.value)
} else {
@@ -52,11 +61,15 @@ export function useDataTableActions(gridApi, rowData) {
}
try {
gridApi.value.startEditingCell({
rowIndex: rowIndex,
colKey: 'fullName'
})
console.log('Started inline editing for row:', rowIndex)
const columnDefs = typeof gridApi.value.getColumnDefs === 'function' ? gridApi.value.getColumnDefs() : []
const firstEditableCol = Array.isArray(columnDefs) ? columnDefs.find(col => col && col.editable && col.field && col.field !== 'action') : null
if (firstEditableCol && typeof gridApi.value.startEditingCell === 'function') {
gridApi.value.ensureIndexVisible?.(rowIndex)
gridApi.value.setFocusedCell?.(rowIndex, firstEditableCol.field)
gridApi.value.startEditingCell({ rowIndex, colKey: firstEditableCol.field })
console.log('Started inline editing for row:', rowIndex)
}
} catch (error) {
console.error('Error starting inline edit:', error)
}
@@ -65,9 +78,10 @@ export function useDataTableActions(gridApi, rowData) {
const deleteRow = (rowIndex, nodeId) => {
console.log('Delete row called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex)
if (rowData) {
deleteRowData.value = { ...rowData }
const rowDataItem = getRowDataByIndex(rowIndex, nodeId)
if (rowDataItem) {
deleteRowData.value = { ...rowDataItem }
deleteRowNodeId.value = nodeId
showDeleteDialog.value = true
console.log('Delete row data:', deleteRowData.value)
} else {
@@ -78,26 +92,22 @@ export function useDataTableActions(gridApi, rowData) {
const confirmDelete = () => {
if (deleteRowData.value && gridApi.value) {
try {
const indexToDelete = rowData.value.findIndex(item => {
return (
(item.id && item.id === deleteRowData.value.id) ||
(item.email && item.email === deleteRowData.value.email) ||
(item.fullName === deleteRowData.value.fullName &&
item.salary === deleteRowData.value.salary)
)
})
// Prefer removing via grid transaction using the exact object
gridApi.value.applyTransaction({ remove: [deleteRowData.value] })
// Also sync our local rowData list. Try by id first, else fallback to object equality
let indexToDelete = -1
if (deleteRowData.value.id != null) {
indexToDelete = rowData.value.findIndex(item => item && item.id === deleteRowData.value.id)
}
if (indexToDelete === -1) {
indexToDelete = rowData.value.findIndex(item => item === deleteRowData.value)
}
if (indexToDelete !== -1) {
rowData.value.splice(indexToDelete, 1)
gridApi.value.applyTransaction({
remove: [deleteRowData.value]
})
console.log('Row deleted successfully')
} else {
console.error('Row not found for deletion')
}
console.log('Row deleted successfully')
} catch (error) {
console.error('Error deleting row:', error)
}
@@ -105,6 +115,7 @@ export function useDataTableActions(gridApi, rowData) {
showDeleteDialog.value = false
deleteRowData.value = null
deleteRowNodeId.value = null
}
const cancelDelete = () => {
@@ -116,25 +127,33 @@ export function useDataTableActions(gridApi, rowData) {
if (!editedData || !gridApi.value) return
try {
const indexToUpdate = rowData.value.findIndex(item => {
return (
(item.id && item.id === editedData.id) ||
(item.email && item.email === selectedRowData.value.email) ||
(item.fullName === selectedRowData.value.fullName)
)
})
if (indexToUpdate !== -1) {
rowData.value[indexToUpdate] = { ...editedData }
const node = gridApi.value.getRowNode(indexToUpdate)
// Update the grid row via nodeId if available
let updated = false
if (selectedRowNodeId.value != null && gridApi.value.getRowNode) {
const node = gridApi.value.getRowNode(selectedRowNodeId.value)
if (node) {
node.setData(editedData)
updated = true
}
console.log('User updated successfully:', editedData)
}
// Sync local rowData list
let indexToUpdate = -1
if (editedData.id != null) {
indexToUpdate = rowData.value.findIndex(item => item && item.id === editedData.id)
}
if (indexToUpdate === -1 && selectedRowData.value) {
indexToUpdate = rowData.value.findIndex(item => item === selectedRowData.value)
}
if (indexToUpdate !== -1) {
rowData.value[indexToUpdate] = { ...editedData }
updated = true
}
if (updated) {
console.log('Row updated successfully:', editedData)
} else {
console.error('User not found for update')
console.error('Row not found for update')
}
} catch (error) {
console.error('Error updating user:', error)
@@ -142,6 +161,7 @@ export function useDataTableActions(gridApi, rowData) {
showDetailsDialog.value = false
selectedRowData.value = null
selectedRowNodeId.value = null
}
const exportToCSV = () => {
@@ -163,7 +183,7 @@ export function useDataTableActions(gridApi, rowData) {
}
const exportToExcel = () => {
if (gridApi.value) {
if (gridApi.value && gridApi.value.exportDataAsExcel) {
gridApi.value.exportDataAsExcel({
fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.xlsx`,
sheetName: 'Data Export',
@@ -176,6 +196,8 @@ export function useDataTableActions(gridApi, rowData) {
allColumns: false,
onlySelectedAllPages: false
})
} else {
console.warn('Excel export not available - requires AG Grid Enterprise')
}
}

View File

@@ -1,47 +1,52 @@
import { ref, computed } from "vue";
import { ref, computed, watch } from "vue";
export function useDataTableGrid(data, isMobile, isTablet) {
export function useDataTableGrid(
data,
isMobile,
isTablet,
customColumns = null
) {
let gridApi = ref(null);
const rowData = ref([]);
const statusCellRenderer = (params) => {
const s = params.value;
let label = "Applied";
let colorClass = "info";
if (s === 1) {
label = "Current";
colorClass = "primary";
}
if (s === 2) {
label = "Professional";
colorClass = "success";
}
if (s === 3) {
label = "Rejected";
colorClass = "error";
}
if (s === 4) {
label = "Resigned";
colorClass = "warning";
}
const statusCellRenderer = (params) => {
const s = params.value;
let label = "Applied";
let colorClass = "info";
const chipSize = isMobile.value ? "x-small" : "small";
const fontSize = isMobile.value ? "10px" : "12px";
if (s === 1) {
label = "Current";
colorClass = "primary";
}
if (s === 2) {
label = "Professional";
colorClass = "success";
}
if (s === 3) {
label = "Rejected";
colorClass = "error";
}
if (s === 4) {
label = "Resigned";
colorClass = "warning";
}
return `<div class="v-chip v-chip--size-${chipSize} v-chip--variant-flat bg-${colorClass}"
style="
height:${isMobile.value ? "20px" : "24px"};
padding:0 ${isMobile.value ? "6px" : "8px"};
display:inline-flex;
align-items:center;
border-radius:12px;
font-size:${fontSize};
font-weight:500;
color:white;
border: inherit;
">${label}</div>`;
};
const chipSize = isMobile.value ? "x-small" : "small";
const fontSize = isMobile.value ? "10px" : "12px";
return `<div class="v-chip v-chip--size-${chipSize} v-chip--variant-flat bg-${colorClass}"
style="
height:${isMobile.value ? "20px" : "24px"};
padding:0 ${isMobile.value ? "6px" : "8px"};
display:inline-flex;
align-items:center;
border-radius:12px;
font-size:${fontSize};
font-weight:500;
color:white;
border: inherit;
">${label}</div>`;
};
const statusCellEditor = () => {
const statusOptions = [
@@ -239,7 +244,7 @@ const statusCellRenderer = (params) => {
`;
};
const columnDefs = ref([
const createDefaultColumns = () => [
{
headerName: "NAME",
field: "fullName",
@@ -253,9 +258,6 @@ const statusCellRenderer = (params) => {
cellEditorParams: {
maxLength: 50,
},
onCellValueChanged: (params) => {
console.log("Name changed:", params.newValue);
},
},
{
headerName: "EMAIL",
@@ -270,9 +272,6 @@ const statusCellRenderer = (params) => {
cellEditorParams: {
maxLength: 100,
},
onCellValueChanged: (params) => {
console.log("Email changed:", params.newValue);
},
},
{
headerName: "DATE",
@@ -284,26 +283,17 @@ const statusCellRenderer = (params) => {
hide: false,
editable: true,
cellEditor: "agDateCellEditor",
cellEditorParams: {
preventEdit: (params) => {
return false;
},
},
valueFormatter: (params) => {
if (!params.value) return "";
const date = new Date(params.value);
if (isNaN(date.getTime())) return "";
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
return `${day}/${month}/${year}`;
},
valueParser: (params) => {
if (!params.newValue) return null;
const dateStr = params.newValue;
if (typeof dateStr === "string" && dateStr.includes("/")) {
const parts = dateStr.split("/");
@@ -311,14 +301,12 @@ const statusCellRenderer = (params) => {
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const year = parseInt(parts[2]);
const date = new Date(year, month, day);
if (!isNaN(date.getTime())) {
return date;
}
}
}
return new Date(dateStr);
},
filterParams: {
@@ -327,14 +315,11 @@ const statusCellRenderer = (params) => {
suppressAndOrCondition: true,
comparator: (filterLocalDateAtMidnight, cellValue) => {
if (!cellValue) return -1;
const cellDate = new Date(cellValue);
cellDate.setHours(0, 0, 0, 0);
if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
return 0;
}
return filterLocalDateAtMidnight < cellDate ? -1 : 1;
},
},
@@ -408,7 +393,6 @@ const statusCellRenderer = (params) => {
precision: 0,
showStepperButtons: false,
},
valueFormatter: (params) => {
if (
params.value === null ||
@@ -420,7 +404,6 @@ const statusCellRenderer = (params) => {
const numValue = Number(params.value);
return isNaN(numValue) ? "" : Math.floor(numValue).toString();
},
onCellValueChanged: (params) => {
const newValue = params.newValue;
@@ -477,11 +460,60 @@ const statusCellRenderer = (params) => {
hide: false,
editable: false,
cellRenderer: actionButtonsRenderer,
suppressMenu: true,
suppressSorting: true,
suppressFilter: true,
suppressHeaderMenuButton: true,
},
]);
];
const resolveColumns = () => {
if (Array.isArray(customColumns)) return customColumns
if (customColumns && Array.isArray(customColumns.value)) return customColumns.value
return []
}
const processCustomColumns = (columns) => {
if (!Array.isArray(columns) || columns.length === 0) {
const base = createDefaultColumns();
return ensureSingleSelectionColumn(base);
}
const processedColumns = columns.map(col => ({
...col,
// Default to inline-editable unless explicitly disabled, skip action column
editable: col.field === 'action' ? false : (col.editable !== undefined ? col.editable : true),
suppressHeaderMenuButton: col.suppressHeaderMenuButton !== undefined ? col.suppressHeaderMenuButton : false,
}));
const withSelection = ensureSingleSelectionColumn(processedColumns);
const hasActionColumn = processedColumns.some(col => col.field === 'action');
if (!hasActionColumn) {
withSelection.push({
headerName: "ACTION",
field: "action",
sortable: false,
filter: false,
flex: 1,
minWidth: 100,
hide: false,
editable: false,
cellRenderer: actionButtonsRenderer,
suppressHeaderMenuButton: true,
});
}
return withSelection;
};
const columnDefs = computed(() => {
const cols = resolveColumns()
if (cols && Array.isArray(cols) && cols.length > 0) {
const processed = processCustomColumns(cols);
return processed;
}
const defaultCols = createDefaultColumns();
return ensureSingleSelectionColumn(defaultCols);
});
const defaultColDef = computed(() => ({
resizable: true,
@@ -494,7 +526,7 @@ const statusCellRenderer = (params) => {
maxWidth: 400,
wrapText: false,
autoHeight: false,
suppressMenu: false,
suppressHeaderMenuButton: false,
}));
const gridOptions = computed(() => ({
@@ -502,12 +534,16 @@ const statusCellRenderer = (params) => {
headerHeight: isMobile.value ? 48 : 56,
rowHeight: isMobile.value ? 44 : 52,
animateRows: true,
rowSelection: { type: "multiRow" },
getRowId: params => {
const value = params.data && (params.data.id ?? params.data.ID ?? params.data._id)
return value != null ? String(value) : String(params.rowIndex)
},
rowSelection: 'multiple',
rowMultiSelectWithClick: true,
suppressRowClickSelection: false,
pagination: true,
paginationPageSize: isMobile.value ? 5 : 10,
paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50],
suppressRowClickSelection: false,
rowMultiSelectWithClick: true,
enableCellTextSelection: true,
suppressHorizontalScroll: false,
alwaysShowHorizontalScroll: false,
@@ -527,55 +563,81 @@ const statusCellRenderer = (params) => {
cacheQuickFilter: true,
enableAdvancedFilter: false,
includeHiddenColumnsInAdvancedFilter: false,
suppressBrowserResizeObserver: false,
maintainColumnOrder: true,
suppressMenuHide: true,
enableRangeSelection: true,
enableFillHandle: false,
enableRangeHandle: false,
loading: false,
suppressNoRowsOverlay: false,
}));
const updateColumnVisibility = () => {
if (!gridApi.value) return;
if (isMobile.value) {
gridApi.value.setColumnVisible("email", false);
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", false);
} else if (isTablet.value) {
gridApi.value.setColumnVisible("email", true);
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", true);
} else {
gridApi.value.setColumnVisible("email", true);
gridApi.value.setColumnVisible("startDate", true);
gridApi.value.setColumnVisible("age", true);
const columns = ['email', 'startDate', 'age'];
try {
columns.forEach(colId => {
const column = gridApi.value.getColumn ? gridApi.value.getColumn(colId) : null;
if (!column) return;
let visible = true;
if (isMobile.value) {
visible = false;
} else if (isTablet.value) {
visible = colId !== 'startDate';
}
if (gridApi.value.setColumnVisible) {
gridApi.value.setColumnVisible(colId, visible);
} else if (gridApi.value.columnApi && gridApi.value.columnApi.setColumnVisible) {
gridApi.value.columnApi.setColumnVisible(colId, visible);
}
});
} catch (error) {
console.warn('Column visibility update not supported in this AG Grid version');
}
};
const updatePagination = () => {
if (!gridApi.value) return;
const pageSize = isMobile.value ? 5 : 10;
const pageSizeSelector = isMobile.value ? [5, 10, 20] : [5, 10, 20, 50];
try {
const gui = gridApi.value.getGui && gridApi.value.getGui();
const containerHeight = gui ? gui.clientHeight : 0;
gridApi.value.paginationSetPageSize(pageSize);
gridApi.value.setGridOption("paginationPageSizeSelector", pageSizeSelector);
const headerH = Number(gridOptions.value.headerHeight || (isMobile.value ? 48 : 56));
// Estimate footer height based on overrides (mobile/desktop)
const footerH = isMobile.value ? 48 : 56;
const rowH = Number(gridOptions.value.rowHeight || (isMobile.value ? 44 : 52));
const padding = 8;
let available = containerHeight - headerH - footerH - padding;
if (!Number.isFinite(available) || available <= 0) {
available = (isMobile.value ? 360 : 520) - headerH - footerH - padding;
}
let rowsPerPage = Math.floor(available / rowH);
rowsPerPage = Math.max(3, Math.min(rowsPerPage, 100));
if (typeof gridApi.value.paginationSetPageSize === 'function') {
gridApi.value.paginationSetPageSize(rowsPerPage);
}
} catch (error) {
console.warn('Error updating pagination:', error);
}
};
function onGridReady(params) {
console.log('Grid ready called with params:', params)
gridApi.value = params.api;
rowData.value = data.map((item) => ({
...item,
salary:
typeof item.salary === "string" ? parseInt(item.salary) : item.salary,
age: typeof item.age === "string" ? parseInt(item.age) : item.age,
}));
console.log('Grid API set:', gridApi.value)
updateColumnVisibility();
updatePagination();
setTimeout(() => {
gridApi.value.sizeColumnsToFit();
if (gridApi.value && gridApi.value.sizeColumnsToFit) {
gridApi.value.sizeColumnsToFit();
}
}, 100);
}
@@ -600,6 +662,17 @@ const statusCellRenderer = (params) => {
}
}
watch([isMobile, isTablet], () => {
updateColumnVisibility();
updatePagination();
});
watch(data, (newData) => {
console.log('Data watch triggered with:', newData)
rowData.value = newData || [];
console.log('rowData updated to:', rowData.value)
}, { immediate: true, deep: true });
return {
gridApi,
columnDefs,
@@ -611,4 +684,36 @@ const statusCellRenderer = (params) => {
updateColumnVisibility,
updatePagination,
};
}
}
// Helpers
function ensureSingleSelectionColumn(columns) {
if (!Array.isArray(columns)) return columns;
const isSelectionCol = (c) => !!(c && (c.checkboxSelection || c.headerCheckboxSelection || c.field === '__selection__' || c.field === 'selection'));
const firstIndex = columns.findIndex(isSelectionCol);
if (firstIndex === -1) return columns;
const unique = [];
columns.forEach((c, idx) => {
if (isSelectionCol(c)) {
if (unique.some(isSelectionCol)) return; // skip duplicates
unique.push({ ...c, pinned: 'left' });
} else {
unique.push(c);
}
});
// Move the selection column to the first position
const selIdx = unique.findIndex(isSelectionCol);
if (selIdx > 0) {
const [sel] = unique.splice(selIdx, 1);
unique.unshift(sel);
}
return unique;
}
// removed selection column injection