feat: reusable ag grid
This commit is contained in:
@@ -1,12 +1,27 @@
|
||||
<script setup>
|
||||
import { onMounted, nextTick } from 'vue'
|
||||
import { onMounted, nextTick, ref } from 'vue'
|
||||
import { GridStack } from 'gridstack'
|
||||
|
||||
import ProjectActivityBarChart from "@/components/ProjectActivityBarChart.vue";
|
||||
import AnalysisCard from "@/components/AnalysisCard.vue";
|
||||
import CostOverview from "@/components/CostOverview.vue";
|
||||
import GeneratedLeadsCard from "@/views/dashboards/ecommerce/EcommerceGeneratedLeads.vue";
|
||||
import GanttChart from './gantt.vue'
|
||||
import AgGridTable from '@/views/demos/forms/tables/data-table/AgGridTable.vue';
|
||||
|
||||
const ganttColumns = ref([
|
||||
{ field: 'id', headerName: 'ID', sortable: true, filter: true, width: 80 },
|
||||
{ field: 'todo', headerName: 'Task', sortable: true, filter: true, flex: 1 },
|
||||
{ field: 'userId', headerName: 'User ID', sortable: true, filter: true, width: 100 },
|
||||
{
|
||||
field: 'completed', headerName: 'Status', sortable: true, filter: true, width: 120, cellRenderer: (params) => {
|
||||
return params.value ? '<span style="color: green; font-weight: bold;">✓ Completed</span>' : '<span style="color: orange; font-weight: bold;">⏳ Pending</span>'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const processAPIData = (response) => {
|
||||
return response.todos || response
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const grid = GridStack.init({
|
||||
@@ -49,9 +64,12 @@ onMounted(async () => {
|
||||
<AnalysisCard />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-stack-item" gs-w="12" gs-h="8" gs-max-h="8">
|
||||
<div class="grid-stack-item" gs-w="12" gs-h="4" gs-max-h="8">
|
||||
<div class="grid-stack-item-content">
|
||||
<GanttChart />
|
||||
<AgGridTable :columns="ganttColumns" api-url="https://dummyjson.com/todos"
|
||||
:data-processor="processAPIData" :enable-export="true" :enable-edit="true" :enable-delete="true"
|
||||
:enable-search="true" height="500px" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,11 +87,11 @@ onMounted(async () => {
|
||||
margin-bottom: 3.2rem;
|
||||
}
|
||||
|
||||
.grid-stack-item:has(GanttChart) {
|
||||
.grid-stack-item:has(AgGridTable) {
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
.grid-stack-item:has(GanttChart) .grid-stack-item-content {
|
||||
.grid-stack-item:has(AgGridTable) .grid-stack-item-content {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import data from '@/views/demos/forms/tables/data-table/datatable'
|
||||
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
||||
import { ExcelExportModule } from 'ag-grid-enterprise'
|
||||
import DataTableHeader from './components/DataTableHeader.vue'
|
||||
import UserDetailsDialog from './components/UserDetailsDialog.vue'
|
||||
import ConfirmDeleteDialog from './components/ConfirmDeleteDialog.vue'
|
||||
@@ -13,199 +11,561 @@ import { useResponsive } from './composables/useResponsive'
|
||||
import jsPDF from 'jspdf'
|
||||
import autoTable from 'jspdf-autotable'
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule, ExcelExportModule])
|
||||
ModuleRegistry.registerModules([AllCommunityModule])
|
||||
|
||||
const props = defineProps({
|
||||
apiUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
apiHeaders: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '600px'
|
||||
},
|
||||
mobileHeight: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
enableExport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
enableEdit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
enableDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
enableSearch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customActions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
dataProcessor: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
schemaMap: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
keepOriginalKeys: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'data-updated',
|
||||
'row-selected',
|
||||
'row-deleted',
|
||||
'row-edited',
|
||||
'export-completed',
|
||||
'api-error'
|
||||
])
|
||||
|
||||
const { isMobile, isTablet, windowWidth } = useResponsive()
|
||||
const gridContainer = ref(null)
|
||||
|
||||
const fetchedData = ref([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
const normalizeRecord = (record, index) => {
|
||||
if (!props.schemaMap || Object.keys(props.schemaMap).length === 0) {
|
||||
return record
|
||||
}
|
||||
const normalized = {}
|
||||
for (const [canonicalKey, candidates] of Object.entries(props.schemaMap)) {
|
||||
const keys = Array.isArray(candidates) ? candidates : [candidates]
|
||||
let found = undefined
|
||||
for (const k of keys) {
|
||||
if (record[k] !== undefined) {
|
||||
found = record[k]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (canonicalKey === 'id' && (found === undefined || found === null)) {
|
||||
normalized[canonicalKey] = record.id ?? index + 1
|
||||
} else {
|
||||
normalized[canonicalKey] = found
|
||||
}
|
||||
}
|
||||
return props.keepOriginalKeys ? { ...record, ...normalized } : normalized
|
||||
}
|
||||
|
||||
const processedData = computed(() => {
|
||||
const sourceData = props.data?.length ? props.data : fetchedData.value
|
||||
if (!Array.isArray(sourceData)) return []
|
||||
|
||||
return sourceData.map((item, index) => {
|
||||
const base = normalizeRecord(item, index)
|
||||
return {
|
||||
...base,
|
||||
id: base?.id ?? item?.id ?? index + 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Dynamically infer columns from data when not provided
|
||||
const inferredColumns = computed(() => {
|
||||
if (Array.isArray(props.columns) && props.columns.length) return props.columns
|
||||
const rows = Array.isArray(processedData.value) ? processedData.value.slice(0, 50) : []
|
||||
if (!rows.length) return []
|
||||
|
||||
// Collect union of keys across sample rows
|
||||
const keySet = new Set()
|
||||
rows.forEach(r => {
|
||||
if (r && typeof r === 'object') {
|
||||
Object.keys(r).forEach(k => {
|
||||
if (k !== 'action' && r[k] !== undefined && typeof r[k] !== 'object') keySet.add(k)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const keys = Array.from(keySet)
|
||||
const toTitle = (k) => k
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/^./, s => s.toUpperCase())
|
||||
|
||||
const isLikelyDate = (v) => {
|
||||
if (typeof v !== 'string') return false
|
||||
// ISO or yyyy-mm-dd or timestamps in string
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(v) || /T\d{2}:\d{2}:\d{2}/.test(v)) return true
|
||||
const d = new Date(v)
|
||||
return !isNaN(d.getTime())
|
||||
}
|
||||
|
||||
return keys.map(k => {
|
||||
// Detect type by scanning a few rows
|
||||
let sampleVal = undefined
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const val = rows[i]?.[k]
|
||||
if (val !== undefined && val !== null) { sampleVal = val; break }
|
||||
}
|
||||
|
||||
const col = {
|
||||
field: k,
|
||||
headerName: toTitle(k),
|
||||
sortable: true,
|
||||
filter: true,
|
||||
editable: false,
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
}
|
||||
|
||||
if (typeof sampleVal === 'number') {
|
||||
col.filter = true
|
||||
col.width = 120
|
||||
col.valueFormatter = p => (typeof p.value === 'number' ? p.value : p.value ?? '')
|
||||
} else if (typeof sampleVal === 'boolean') {
|
||||
col.width = 120
|
||||
col.valueFormatter = p => (p.value ? 'Yes' : 'No')
|
||||
} else if (isLikelyDate(sampleVal)) {
|
||||
col.width = 140
|
||||
col.valueFormatter = p => {
|
||||
if (!p.value) return ''
|
||||
const d = new Date(p.value)
|
||||
return isNaN(d.getTime()) ? String(p.value) : d.toISOString().split('T')[0]
|
||||
}
|
||||
} else {
|
||||
col.flex = 1
|
||||
}
|
||||
return col
|
||||
})
|
||||
})
|
||||
|
||||
// Build grid with inferred columns (or props.columns if provided)
|
||||
// Note: Passing inferredColumns.value at setup time; if you need live re-init on schema change, we can add a watcher.
|
||||
let gridSetup = useDataTableGrid(processedData, isMobile, isTablet, inferredColumns)
|
||||
const gridApi = gridSetup.gridApi
|
||||
const columnDefs = gridSetup.columnDefs
|
||||
const rowData = gridSetup.rowData
|
||||
const defaultColDef = gridSetup.defaultColDef
|
||||
const gridOptions = gridSetup.gridOptions
|
||||
const gridReady = gridSetup.onGridReady
|
||||
const onCellValueChanged = gridSetup.onCellValueChanged
|
||||
const updateColumnVisibility = gridSetup.updateColumnVisibility
|
||||
const updatePagination = gridSetup.updatePagination
|
||||
|
||||
watch(
|
||||
processedData,
|
||||
(newData) => {
|
||||
if (Array.isArray(newData) && gridApi.value) {
|
||||
try {
|
||||
if (typeof gridApi.value.setRowData === 'function') {
|
||||
gridApi.value.setRowData(newData)
|
||||
} else if (typeof gridApi.value.setGridOption === 'function') {
|
||||
gridApi.value.setGridOption("rowData", newData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error updating grid data:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!props.apiUrl) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await fetch(props.apiUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...props.apiHeaders
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
let processedApiData = []
|
||||
if (props.dataProcessor && typeof props.dataProcessor === 'function') {
|
||||
processedApiData = props.dataProcessor(result)
|
||||
} else {
|
||||
processedApiData = result.todos || result.data || result.users || result.products || (Array.isArray(result) ? result : [])
|
||||
}
|
||||
|
||||
fetchedData.value = Array.isArray(processedApiData) ? processedApiData : []
|
||||
console.log('Fetched data set to:', fetchedData.value)
|
||||
emit('data-updated', fetchedData.value)
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
emit('api-error', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveToAPI = async (userData, isUpdate = false) => {
|
||||
if (!props.apiUrl) return
|
||||
|
||||
try {
|
||||
const method = isUpdate ? 'PUT' : 'POST'
|
||||
const url = isUpdate ? `${props.apiUrl}/${userData.id}` : props.apiUrl
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...props.apiHeaders
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error saving data:', error)
|
||||
emit('api-error', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFromAPI = async (id) => {
|
||||
if (!props.apiUrl) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`${props.apiUrl}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...props.apiHeaders
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error deleting data:', error)
|
||||
emit('api-error', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
gridApi,
|
||||
columnDefs,
|
||||
rowData,
|
||||
defaultColDef,
|
||||
gridOptions,
|
||||
onGridReady: gridReady,
|
||||
onCellValueChanged,
|
||||
updateColumnVisibility,
|
||||
updatePagination
|
||||
} = useDataTableGrid(data, isMobile, isTablet)
|
||||
|
||||
const {
|
||||
setupGlobalHandlers,
|
||||
cleanupGlobalHandlers,
|
||||
exportToCSV,
|
||||
exportToExcel,
|
||||
saveUser,
|
||||
confirmDelete,
|
||||
cancelDelete,
|
||||
showDetailsDialog,
|
||||
selectedRowData,
|
||||
deleteRowData,
|
||||
showDeleteDialog,
|
||||
deleteRow
|
||||
setupGlobalHandlers,
|
||||
cleanupGlobalHandlers,
|
||||
exportToCSV,
|
||||
exportToExcel,
|
||||
saveUser,
|
||||
confirmDelete,
|
||||
cancelDelete,
|
||||
showDetailsDialog,
|
||||
selectedRowData,
|
||||
deleteRowData,
|
||||
showDeleteDialog,
|
||||
deleteRow
|
||||
} = useDataTableActions(gridApi, rowData)
|
||||
|
||||
const handleSaveUser = async (updatedData) => {
|
||||
try {
|
||||
// Optimistic update: update grid first
|
||||
saveUser(updatedData)
|
||||
|
||||
if (props.apiUrl) {
|
||||
try {
|
||||
await saveToAPI(updatedData, updatedData.id ? true : false)
|
||||
} catch (apiErr) {
|
||||
console.error('API save failed, refetching to sync:', apiErr)
|
||||
// Re-sync from server on failure
|
||||
await fetchData()
|
||||
emit('api-error', apiErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('row-edited', updatedData)
|
||||
} catch (error) {
|
||||
console.error('Error saving user:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
try {
|
||||
// Optimistic remove from grid first
|
||||
confirmDelete()
|
||||
|
||||
if (props.apiUrl && deleteRowData.value?.id) {
|
||||
try {
|
||||
await deleteFromAPI(deleteRowData.value.id)
|
||||
} catch (apiErr) {
|
||||
console.error('API delete failed, refetching to restore:', apiErr)
|
||||
await fetchData()
|
||||
emit('api-error', apiErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('row-deleted', deleteRowData.value)
|
||||
} catch (error) {
|
||||
console.error('Error deleting row:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const quickFilter = ref('')
|
||||
|
||||
watch(quickFilter, (newValue) => {
|
||||
if (gridApi.value) {
|
||||
gridApi.value.setGridOption('quickFilterText', newValue || '')
|
||||
}
|
||||
if (gridApi.value) {
|
||||
gridApi.value.setGridOption('quickFilterText', newValue || '')
|
||||
}
|
||||
}, { immediate: false })
|
||||
|
||||
const onGridReady = params => {
|
||||
gridReady(params)
|
||||
setupGlobalHandlers()
|
||||
}
|
||||
gridReady(params)
|
||||
setupGlobalHandlers()
|
||||
|
||||
if (props.apiUrl) {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSaveUser = updatedData => saveUser(updatedData)
|
||||
// Observe container size to recalc pagination
|
||||
try {
|
||||
const container = gridContainer?.value
|
||||
if (container && typeof ResizeObserver !== 'undefined') {
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (gridApi.value) {
|
||||
updatePagination()
|
||||
setTimeout(() => {
|
||||
gridApi.value?.sizeColumnsToFit?.()
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
ro.observe(container)
|
||||
}
|
||||
} catch (e) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickFilterUpdate = (value) => {
|
||||
quickFilter.value = value || ''
|
||||
|
||||
if (gridApi.value) {
|
||||
gridApi.value.setGridOption('quickFilterText', value || '')
|
||||
}
|
||||
quickFilter.value = value || ''
|
||||
|
||||
if (gridApi.value) {
|
||||
gridApi.value.setGridOption('quickFilterText', value || '')
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
if (props.apiUrl) {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refreshData,
|
||||
fetchData,
|
||||
gridApi
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setupGlobalHandlers()
|
||||
window.addEventListener('resize', handleResize)
|
||||
setupGlobalHandlers()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupGlobalHandlers()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
cleanupGlobalHandlers()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
windowWidth.value = window.innerWidth
|
||||
if (gridApi.value) {
|
||||
updateColumnVisibility()
|
||||
updatePagination()
|
||||
setTimeout(() => {
|
||||
if (gridApi.value && gridApi.value.sizeColumnsToFit) {
|
||||
gridApi.value.sizeColumnsToFit()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
windowWidth.value = window.innerWidth
|
||||
if (gridApi.value) {
|
||||
updateColumnVisibility()
|
||||
updatePagination()
|
||||
setTimeout(() => {
|
||||
if (gridApi.value && gridApi.value.sizeColumnsToFit) {
|
||||
gridApi.value.sizeColumnsToFit()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
const exportToPDF = () => {
|
||||
if (!gridApi.value) {
|
||||
console.error('Grid API not available')
|
||||
return
|
||||
}
|
||||
if (!gridApi.value) {
|
||||
console.error('Grid API not available')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const doc = new jsPDF()
|
||||
const rowDataForPDF = []
|
||||
const headers = []
|
||||
|
||||
columnDefs.value.forEach(col => {
|
||||
if (col.headerName && col.field && col.field !== 'action') {
|
||||
headers.push(col.headerName)
|
||||
}
|
||||
})
|
||||
|
||||
gridApi.value.forEachNodeAfterFilterAndSort(node => {
|
||||
const row = []
|
||||
columnDefs.value.forEach(col => {
|
||||
if (col.field && col.field !== 'action') {
|
||||
let value = node.data[col.field] || ''
|
||||
|
||||
if (col.field === 'salary' && value) {
|
||||
value = `$${Math.floor(Number(value)).toLocaleString()}`
|
||||
} else if (col.field === 'startDate' && value) {
|
||||
const date = new Date(value)
|
||||
if (!isNaN(date.getTime())) {
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const year = date.getFullYear()
|
||||
value = `${day}/${month}/${year}`
|
||||
}
|
||||
} else if (col.field === 'status') {
|
||||
const statusMap = {
|
||||
0: 'Applied',
|
||||
1: 'Current',
|
||||
2: 'Professional',
|
||||
3: 'Rejected',
|
||||
4: 'Resigned'
|
||||
}
|
||||
value = statusMap[value] || 'Applied'
|
||||
}
|
||||
|
||||
row.push(value)
|
||||
}
|
||||
})
|
||||
rowDataForPDF.push(row)
|
||||
})
|
||||
|
||||
autoTable(doc, {
|
||||
head: [headers],
|
||||
body: rowDataForPDF,
|
||||
styles: {
|
||||
fontSize: 8,
|
||||
cellPadding: 2
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [41, 128, 185],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold'
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [245, 245, 245]
|
||||
},
|
||||
margin: { top: 20 }
|
||||
})
|
||||
|
||||
doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`)
|
||||
console.log('PDF exported successfully')
|
||||
} catch (error) {
|
||||
console.error('Error exporting PDF:', error)
|
||||
}
|
||||
try {
|
||||
const doc = new jsPDF()
|
||||
const rowDataForPDF = []
|
||||
const headers = []
|
||||
|
||||
columnDefs.value.forEach(col => {
|
||||
if (col.headerName && col.field && col.field !== 'action') {
|
||||
headers.push(col.headerName)
|
||||
}
|
||||
})
|
||||
|
||||
gridApi.value.forEachNodeAfterFilterAndSort(node => {
|
||||
const row = []
|
||||
columnDefs.value.forEach(col => {
|
||||
if (col.field && col.field !== 'action') {
|
||||
let value = node.data[col.field] || ''
|
||||
row.push(value)
|
||||
}
|
||||
})
|
||||
rowDataForPDF.push(row)
|
||||
})
|
||||
|
||||
autoTable(doc, {
|
||||
head: [headers],
|
||||
body: rowDataForPDF,
|
||||
styles: {
|
||||
fontSize: 8,
|
||||
cellPadding: 2
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [41, 128, 185],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold'
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [245, 245, 245]
|
||||
},
|
||||
margin: { top: 20 }
|
||||
})
|
||||
|
||||
doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`)
|
||||
emit('export-completed', 'pdf')
|
||||
} catch (error) {
|
||||
console.error('Error exporting PDF:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="elevation-2">
|
||||
<DataTableHeader
|
||||
:is-mobile="isMobile"
|
||||
:is-tablet="isTablet"
|
||||
:quick-filter="quickFilter"
|
||||
@update:quick-filter="handleQuickFilterUpdate"
|
||||
@export-csv="exportToCSV"
|
||||
@export-excel="exportToExcel"
|
||||
@export-pdf="exportToPDF"
|
||||
<v-card class="elevation-2">
|
||||
<v-overlay
|
||||
v-model="isLoading"
|
||||
contained
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="64"
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
</v-overlay>
|
||||
|
||||
<DataTableHeader
|
||||
v-if="enableSearch || enableExport"
|
||||
:is-mobile="isMobile"
|
||||
:is-tablet="isTablet"
|
||||
:quick-filter="quickFilter"
|
||||
:enable-search="enableSearch"
|
||||
:enable-export="enableExport"
|
||||
@update:quick-filter="handleQuickFilterUpdate"
|
||||
@export-csv="exportToCSV"
|
||||
@export-excel="exportToExcel"
|
||||
@export-pdf="exportToPDF"
|
||||
/>
|
||||
|
||||
<v-divider v-if="enableSearch || enableExport" />
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<AgGridVue
|
||||
class="vuetify-grid ag-theme-alpine-dark"
|
||||
:style="`height:${isMobile ? mobileHeight : height}; width:100%`"
|
||||
:columnDefs="columnDefs"
|
||||
:rowData="processedData"
|
||||
:defaultColDef="defaultColDef"
|
||||
:gridOptions="gridOptions"
|
||||
@grid-ready="onGridReady"
|
||||
@cell-value-changed="onCellValueChanged"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
<UserDetailsDialog
|
||||
v-if="enableEdit"
|
||||
v-model="showDetailsDialog"
|
||||
:selected-row-data="selectedRowData"
|
||||
:columns="columnDefs"
|
||||
:is-mobile="isMobile"
|
||||
@save-user="handleSaveUser"
|
||||
/>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<AgGridVue
|
||||
class="vuetify-grid ag-theme-alpine-dark"
|
||||
:style="`height:${isMobile ? '400px' : '600px'}; width:100%`"
|
||||
:columnDefs="columnDefs"
|
||||
:rowData="rowData"
|
||||
:defaultColDef="defaultColDef"
|
||||
:gridOptions="gridOptions"
|
||||
@grid-ready="onGridReady"
|
||||
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<UserDetailsDialog
|
||||
v-model="showDetailsDialog"
|
||||
:selected-row-data="selectedRowData"
|
||||
:is-mobile="isMobile"
|
||||
@save-user="handleSaveUser"
|
||||
/>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
v-model="showDeleteDialog"
|
||||
:selected-row-data="deleteRowData"
|
||||
:is-mobile="isMobile"
|
||||
@confirm="confirmDelete"
|
||||
@cancel="cancelDelete"
|
||||
/>
|
||||
</v-card>
|
||||
<ConfirmDeleteDialog
|
||||
v-if="enableDelete"
|
||||
v-model="showDeleteDialog"
|
||||
:selected-row-data="deleteRowData"
|
||||
:columns="columnDefs"
|
||||
:is-mobile="isMobile"
|
||||
@confirm="handleConfirmDelete"
|
||||
@cancel="cancelDelete"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<v-card-text class="py-6">
|
||||
<div class="text-body-1 mb-4">
|
||||
Are you sure you want to delete this user?
|
||||
Are you sure you want to delete this record?
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
@@ -24,15 +24,14 @@
|
||||
class="pa-3 bg-grey-lighten-5"
|
||||
>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon icon="tabler-user" size="small" class="me-2" />
|
||||
<strong class="me-2">Name:</strong>
|
||||
{{ selectedRowData.fullName || 'Unknown' }}
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon icon="tabler-mail" size="small" class="me-2" />
|
||||
<strong class="me-2">Email:</strong>
|
||||
{{ selectedRowData.email || 'Unknown' }}
|
||||
<div
|
||||
v-for="(val, key) in previewFields"
|
||||
:key="key"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<VIcon icon="tabler-dot" size="small" class="me-2" />
|
||||
<strong class="me-2">{{ key }}:</strong>
|
||||
{{ formatValue(val) }}
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
@@ -85,7 +84,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -97,6 +96,10 @@ defineProps({
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
columns: {
|
||||
type: [Array, Object],
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -118,6 +121,26 @@ const handleConfirm = () => {
|
||||
emit('update:model-value', false)
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
const previewFields = computed(() => {
|
||||
const cols = Array.isArray(props.columns) ? props.columns : (props.columns?.value || [])
|
||||
const prefer = ['title','name','fullName','todo','email','id','userId']
|
||||
const keys = []
|
||||
for (const p of prefer) {
|
||||
if (props.selectedRowData && props.selectedRowData[p] !== undefined) keys.push(p)
|
||||
}
|
||||
for (const c of cols) {
|
||||
if (c.field && !keys.includes(c.field) && c.field !== 'action' && keys.length < 4) keys.push(c.field)
|
||||
}
|
||||
const map = {}
|
||||
keys.forEach(k => { map[k] = props.selectedRowData?.[k] })
|
||||
return map
|
||||
})
|
||||
|
||||
const formatValue = (v) => {
|
||||
if (Array.isArray(v) || typeof v === 'object') return JSON.stringify(v)
|
||||
return v ?? 'N/A'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
selectedRowData: Object,
|
||||
isMobile: Boolean
|
||||
isMobile: Boolean,
|
||||
columns: {
|
||||
type: [Array, Object],
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save-user'])
|
||||
@@ -111,6 +115,11 @@ const getFieldIcon = (field) => {
|
||||
}
|
||||
return iconMap[field] || 'tabler-info-circle'
|
||||
}
|
||||
|
||||
const visibleColumns = computed(() => {
|
||||
const list = Array.isArray(props.columns) ? props.columns : (props.columns?.value || [])
|
||||
return list.filter(c => c.field && c.field !== 'action')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -179,7 +188,11 @@ const getFieldIcon = (field) => {
|
||||
<v-form @keyup.enter="handleEnterKey">
|
||||
<v-container fluid class="pa-0">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" class="mb-2">
|
||||
<v-col
|
||||
v-for="col in visibleColumns"
|
||||
:key="col.field"
|
||||
cols="12" sm="6" class="mb-2"
|
||||
>
|
||||
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
@@ -192,212 +205,35 @@ const getFieldIcon = (field) => {
|
||||
border: '1px solid rgba(var(--v-theme-primary), 0.3)'
|
||||
}"
|
||||
>
|
||||
<v-icon :icon="getFieldIcon('name')" color="primary" />
|
||||
<v-icon :icon="getFieldIcon(col.field)" color="primary" />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Full Name</v-card-subtitle>
|
||||
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
|
||||
{{ selectedRowData.fullName || 'N/A' }}
|
||||
</v-card-title>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editedData.fullName"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="mb-2">
|
||||
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
size="40"
|
||||
:style="{
|
||||
background: 'rgba(var(--v-theme-secondary), 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
webkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(var(--v-theme-secondary), 0.3)'
|
||||
}"
|
||||
>
|
||||
<v-icon :icon="getFieldIcon('email')" color="secondary" />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Email Address</v-card-subtitle>
|
||||
<v-card-title v-if="!isEditMode" class="pa-0 text-body-1 text-high-emphasis">
|
||||
{{ selectedRowData.email || 'N/A' }}
|
||||
</v-card-title>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editedData.email"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
type="email"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="mb-2">
|
||||
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
size="40"
|
||||
:style="{
|
||||
background: 'rgba(var(--v-theme-info), 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
webkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(var(--v-theme-info), 0.3)'
|
||||
}"
|
||||
>
|
||||
<v-icon :icon="getFieldIcon('startDate')" color="info" />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Start Date</v-card-subtitle>
|
||||
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
|
||||
{{ formatDate(selectedRowData.startDate) }}
|
||||
</v-card-title>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editedData.startDate"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
type="date"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="mb-2">
|
||||
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
size="40"
|
||||
:style="{
|
||||
background: 'rgba(var(--v-theme-success), 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
webkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(var(--v-theme-success), 0.3)'
|
||||
}"
|
||||
>
|
||||
<v-icon :icon="getFieldIcon('salary')" color="success" />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Salary</v-card-subtitle>
|
||||
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-success font-weight-bold">
|
||||
{{ formatSalary(selectedRowData.salary) }}
|
||||
</v-card-title>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editedData.salary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
type="number"
|
||||
prefix="$"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="mb-2">
|
||||
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
size="40"
|
||||
:style="{
|
||||
background: 'rgba(var(--v-theme-warning), 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
webkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(var(--v-theme-warning), 0.3)'
|
||||
}"
|
||||
>
|
||||
<v-icon :icon="getFieldIcon('age')" color="warning" />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Age</v-card-subtitle>
|
||||
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
|
||||
{{ selectedRowData.age ? selectedRowData.age + ' years' : 'N/A' }}
|
||||
</v-card-title>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editedData.age"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
type="number"
|
||||
suffix="years"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="mb-2">
|
||||
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
size="40"
|
||||
:style="{
|
||||
background: `rgba(var(--v-theme-${getStatusColor(isEditMode ? editedData.status : selectedRowData.status)}), 0.2)`,
|
||||
backdropFilter: 'blur(10px)',
|
||||
webkitBackdropFilter: 'blur(10px)',
|
||||
border: `1px solid rgba(var(--v-theme-${getStatusColor(isEditMode ? editedData.status : selectedRowData.status)}), 0.3)`
|
||||
}"
|
||||
>
|
||||
<v-icon :icon="getFieldIcon('status')" :color="getStatusColor(isEditMode ? editedData.status : selectedRowData.status)" />
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Status</v-card-subtitle>
|
||||
<div v-if="!isEditMode" class="pt-1">
|
||||
<v-chip
|
||||
:color="getStatusColor(selectedRowData.status)"
|
||||
size="default"
|
||||
variant="flat"
|
||||
class="font-weight-medium"
|
||||
>
|
||||
<v-icon start icon="tabler-check" size="small" />
|
||||
{{ getStatusLabel(selectedRowData.status) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-select
|
||||
v-else
|
||||
v-model="editedData.status"
|
||||
:items="statusOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mt-1"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.title">
|
||||
<template #prepend>
|
||||
<v-chip :color="item.raw.color" size="small" class="me-2">{{ item.title }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #selection="{ item }">
|
||||
<v-chip :color="item.raw.color" size="small">{{ item.title }}</v-chip>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">{{ col.headerName || col.field }}</v-card-subtitle>
|
||||
<template v-if="!isEditMode">
|
||||
<v-card-title class="pa-0 text-body-1 text-high-emphasis">
|
||||
{{ Array.isArray(selectedRowData[col.field]) || typeof selectedRowData[col.field] === 'object' ? JSON.stringify(selectedRowData[col.field]) : (selectedRowData[col.field] ?? 'N/A') }}
|
||||
</v-card-title>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-text-field
|
||||
v-if="col.editable !== false && col.field !== 'status'"
|
||||
v-model="editedData[col.field]"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mt-1"
|
||||
:type="col.field.toLowerCase().includes('date') ? 'date' : (typeof selectedRowData[col.field] === 'number' ? 'number' : 'text')"
|
||||
/>
|
||||
<v-select
|
||||
v-else-if="col.field === 'status'"
|
||||
v-model="editedData.status"
|
||||
:items="statusOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mt-1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,320 @@
|
||||
.ag-theme-alpine-dark .ag-cell {
|
||||
cursor: text !important;
|
||||
}
|
||||
.ag-theme-alpine.vuetify-grid .ag-cell,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-cell {
|
||||
-webkit-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
user-select: text !important;
|
||||
}
|
||||
|
||||
/* Inputs (filters, quick filter, tool panels) */
|
||||
.ag-theme-alpine.vuetify-grid .ag-input-field-input,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-input-field-input {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.04) !important;
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 6px 8px !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
box-sizing: border-box !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-input-field-input:focus,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-input-field-input:focus {
|
||||
outline: none !important;
|
||||
border-color: rgb(var(--v-theme-primary)) !important;
|
||||
box-shadow: 0 0 0 3px rgba(var(--v-theme-primary), 0.2) !important;
|
||||
}
|
||||
|
||||
/* Checkboxes (row select, set filter items) */
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
border-radius: 6px !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
border-radius: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
position: static !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked { transform: none; }
|
||||
|
||||
/* keep AG Grid's own checkmark */
|
||||
|
||||
/* Center and tidy header selection checkbox */
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-select-all,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-select-all {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-selection-checkbox,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-selection-checkbox {
|
||||
margin: 0 auto !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
/* Center selection checkbox inside body cells as well */
|
||||
.ag-theme-alpine.vuetify-grid .ag-cell .ag-selection-checkbox,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-cell .ag-selection-checkbox {
|
||||
margin: 0 auto !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
/* Prevent nested visual duplication: wrapper stays transparent, input renders the box */
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-checkbox-input-wrapper,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-checkbox-input-wrapper {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
/* Unify checkbox input look & interactions */
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:hover,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:hover {
|
||||
border-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:focus-visible,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 3px rgba(var(--v-theme-primary), 0.25) !important;
|
||||
}
|
||||
|
||||
/* Improve alignment for narrow first column without forcing brittle indices */
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-cell.ag-column-first,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell.ag-column-first {
|
||||
padding-left: 6px !important;
|
||||
padding-right: 6px !important;
|
||||
}
|
||||
|
||||
/* Make selection column narrower and center contents */
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-cell.ag-column-first,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell.ag-column-first {
|
||||
padding-left: 6px !important;
|
||||
padding-right: 6px !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-center-cols-container .ag-cell.ag-cell-not-inline-editing.ag-cell-focus.ag-cell-value.ag-cell-normal-height[aria-colindex="1"],
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-center-cols-container .ag-cell.ag-cell-not-inline-editing.ag-cell-focus.ag-cell-value.ag-cell-normal-height[aria-colindex="1"] {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
/* Set filter items aesthetics */
|
||||
.ag-theme-alpine.vuetify-grid .ag-set-filter-item,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-set-filter-item {
|
||||
padding: 6px 8px !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-set-filter-item:hover,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-set-filter-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08) !important;
|
||||
}
|
||||
|
||||
/* Status cell: ensure chip text aligns nicely */
|
||||
.ag-theme-alpine.vuetify-grid .ag-cell[col-id="status"],
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-cell[col-id="status"] {
|
||||
padding-top: 6px !important;
|
||||
padding-bottom: 6px !important;
|
||||
}
|
||||
.ag-theme-alpine .ag-cell,
|
||||
.ag-theme-alpine-dark .ag-cell {
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
/* Set Filter, Column Tool Panel, Picker styles to match panel */
|
||||
.ag-theme-alpine .ag-menu,
|
||||
.ag-theme-alpine .ag-filter,
|
||||
.ag-theme-alpine .ag-set-filter,
|
||||
.ag-theme-alpine .ag-column-panel,
|
||||
.ag-theme-alpine .ag-tool-panel-wrapper,
|
||||
.ag-theme-alpine .ag-picker-field-wrapper {
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-menu,
|
||||
.ag-theme-alpine-dark .ag-filter,
|
||||
.ag-theme-alpine-dark .ag-set-filter,
|
||||
.ag-theme-alpine-dark .ag-column-panel,
|
||||
.ag-theme-alpine-dark .ag-tool-panel-wrapper,
|
||||
.ag-theme-alpine-dark .ag-picker-field-wrapper {
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-set-filter-item,
|
||||
.ag-theme-alpine-dark .ag-set-filter-item {
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
}
|
||||
|
||||
/* Target the exact AG Grid checkbox structure you shared */
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper {
|
||||
position: static !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper:hover,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper:hover { transform: none; }
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked { transform: none; }
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked::after,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked::after { content: none !important; }
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-field-input,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-field-input {
|
||||
position: static !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-field-input:focus-visible + .ag-input-wrapper,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-field-input:focus-visible + .ag-input-wrapper { transform: none; }
|
||||
|
||||
/* Ensure horizontal/vertical centering of checkbox in header and cells */
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-checkbox,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-checkbox,
|
||||
.ag-theme-alpine.vuetify-grid .ag-cell .ag-checkbox,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-cell .ag-checkbox {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Final neutralization: let AG Grid draw the checkbox visuals */
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-set-filter-item:hover,
|
||||
.ag-theme-alpine-dark .ag-set-filter-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08) !important;
|
||||
}
|
||||
|
||||
/* Remove generic checkbox box styling to avoid double visuals; rely on theme defaults */
|
||||
.ag-theme-alpine .ag-checkbox-input-wrapper input[type="checkbox"],
|
||||
.ag-theme-alpine-dark .ag-checkbox-input-wrapper input[type="checkbox"] {
|
||||
background: initial !important;
|
||||
border: initial !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-checkbox-input-wrapper input[type="checkbox"]:checked,
|
||||
.ag-theme-alpine-dark .ag-checkbox-input-wrapper input[type="checkbox"]:checked {
|
||||
background: initial !important;
|
||||
border-color: initial !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-selection-checkbox,
|
||||
.ag-theme-alpine-dark .ag-selection-checkbox {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-header-cell-menu-button,
|
||||
.ag-theme-alpine-dark .ag-header-cell-menu-button {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-header-cell:hover .ag-icon-filter,
|
||||
.ag-theme-alpine-dark .ag-header-cell:hover .ag-icon-filter {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-picker-field-display,
|
||||
.ag-theme-alpine-dark .ag-picker-field-display {
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-select .ag-picker-field-wrapper,
|
||||
.ag-theme-alpine-dark .ag-select .ag-picker-field-wrapper {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.05) !important;
|
||||
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 4px 8px !important;
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-list-item,
|
||||
.ag-theme-alpine-dark .ag-list-item {
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-cell:hover {
|
||||
cursor: text !important;
|
||||
@@ -150,45 +464,25 @@
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
border: 2px solid rgba(var(--v-theme-on-surface), 0.6) !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
border-color: rgb(var(--v-theme-primary)) !important;
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
border-radius: 4px !important;
|
||||
border: 2px solid rgba(var(--v-theme-on-surface), 0.6) !important;
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
position: relative !important;
|
||||
transition: all 0.2s ease-in-out !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
border-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked { transform: none; }
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked::after {
|
||||
content: "✓" !important;
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
color: rgb(var(--v-theme-on-primary)) !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked::after { content: none !important; }
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:hover {
|
||||
border-color: rgb(var(--v-theme-primary)) !important;
|
||||
background-color: rgba(var(--v-theme-primary), 0.08) !important;
|
||||
}
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:hover { transform: none; }
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-menu {
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
@@ -313,7 +607,31 @@
|
||||
justify-content: space-between !important;
|
||||
transition: all 0.2s ease-in-out !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure page-size picker can overflow and is clickable */
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-paging-page-size,
|
||||
.ag-theme-alpine.vuetify-grid .ag-paging-page-size {
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-wrapper.ag-picker-field-wrapper,
|
||||
.ag-theme-alpine.vuetify-grid .ag-wrapper.ag-picker-field-wrapper {
|
||||
position: relative !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Raise ag-Grid popups above footer */
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-popup,
|
||||
.ag-theme-alpine.vuetify-grid .ag-popup,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-select-list,
|
||||
.ag-theme-alpine.vuetify-grid .ag-select-list,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-menu,
|
||||
.ag-theme-alpine.vuetify-grid .ag-menu {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-paging-panel:hover {
|
||||
@@ -653,7 +971,7 @@
|
||||
justify-content: space-between !important;
|
||||
transition: all 0.2s ease-in-out !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
overflow: visible !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
@@ -998,4 +1316,73 @@
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-paging-page-summary-panel,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-paging-row-summary-panel {
|
||||
color: rgb(var(--v-theme-on-surface)) !important;
|
||||
}
|
||||
|
||||
/* === FINAL MINIMAL CHECKBOX FIXES (reset visuals, keep centering) === */
|
||||
/* Reset wrappers/inputs to AG Grid default rendering so no double visuals occur */
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper,
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper,
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
position: static !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
|
||||
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
|
||||
position: static !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
opacity: 1 !important;
|
||||
cursor: pointer !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Center selection checkboxes in header and cells without altering visuals */
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-header-select-all,
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-header-select-all {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Ensure checkbox container is centered */
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox,
|
||||
.ag-theme-alpine.vuetify-grid .ag-selection-checkbox,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-header-select-all,
|
||||
.ag-theme-alpine.vuetify-grid .ag-header-select-all,
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-cell .ag-checkbox,
|
||||
.ag-theme-alpine.vuetify-grid .ag-cell .ag-checkbox {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark.vuetify-grid .ag-centered,
|
||||
.ag-theme-alpine.vuetify-grid .ag-centered {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* Optional: use AG Grid var to unify checkbox size if supported */
|
||||
.ag-theme-alpine-dark.vuetify-grid,
|
||||
.ag-theme-alpine.vuetify-grid {
|
||||
--ag-checkbox-size: 18px;
|
||||
}
|
||||
Reference in New Issue
Block a user