fix(fe): KsDataGrid + CommonCodeManagementPage - grid edit issues
Fixed 3 critical bugs in grid editing: 1. onCellValueChanged redrawRows() removal - Removed event.api.redrawRows() that was resetting cell input - Issue: redrawRows() triggered computed property re-evaluation - Result: Array reference changed, grid lost input value 2. ScrollApiModule registration - Added ScrollApiModule to ModuleRegistry - Issue: focusRow() called ensureIndexVisible without module - Result: AG Grid #200 error, page hung 3. onCellEditingStopped removal - Removed auto-restart of edit mode on cell exit - Issue: Prevented navigateToNextCell from working on Enter key - Result: Enter key now properly moves focus to next cell 4. CommonCodeManagementPage focusRow safety - Wrapped focusRow() in try-catch - Issue: focusRow may not be available, causing errors - Result: Grid continues even if focusRow unavailable Affects: /system/common-codes grid editing and all pages using KsDataGrid Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,11 +236,11 @@ const saveMasterBatch = () => {
|
||||
// Inline Child Code Grid Editing Handlers (Top-Row Insertion)
|
||||
const addGridRow = () => {
|
||||
const groupCodeKey = selectedGroupCode.value.groupCode
|
||||
let list = mockChildCodesMap[groupCodeKey]
|
||||
if (!list) {
|
||||
list = []
|
||||
Object.assign(mockChildCodesMap, { [groupCodeKey]: list })
|
||||
if (!mockChildCodesMap[groupCodeKey]) {
|
||||
mockChildCodesMap[groupCodeKey] = reactive<CommonCode[]>([])
|
||||
}
|
||||
const list = mockChildCodesMap[groupCodeKey]
|
||||
|
||||
const newRow: CommonCode = {
|
||||
code: `NEW_CODE_${list.length + 1}`,
|
||||
codeName: '새 코드명',
|
||||
@@ -256,7 +256,11 @@ const addGridRow = () => {
|
||||
selectedGroupCode.value.codeCount = list.length
|
||||
|
||||
nextTick(() => {
|
||||
detailGridRef.value?.focusRow(0, 'code')
|
||||
try {
|
||||
detailGridRef.value?.focusRow?.(0, 'code')
|
||||
} catch (e) {
|
||||
console.warn('focusRow not available', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import { ClientSideRowModelModule, ColumnAutoSizeModule, CellStyleModule, ValidationModule, ModuleRegistry, RowSelectionModule, TextFilterModule, themeQuartz, type ColDef, type RowClickedEvent } from 'ag-grid-community'
|
||||
import {
|
||||
ClientSideRowModelModule,
|
||||
ColumnAutoSizeModule,
|
||||
CellStyleModule,
|
||||
ValidationModule,
|
||||
TextFilterModule,
|
||||
TextEditorModule,
|
||||
NumberEditorModule,
|
||||
SelectEditorModule,
|
||||
UndoRedoEditModule,
|
||||
RowStyleModule,
|
||||
RenderApiModule,
|
||||
ModuleRegistry,
|
||||
RowSelectionModule,
|
||||
ScrollApiModule,
|
||||
themeQuartz,
|
||||
type ColDef,
|
||||
type RowClickedEvent,
|
||||
type CellValueChangedEvent,
|
||||
type RowClassParams
|
||||
} from 'ag-grid-community'
|
||||
import type { UiGridColumn } from '../adapter/contracts'
|
||||
|
||||
ModuleRegistry.registerModules([ClientSideRowModelModule, ColumnAutoSizeModule, CellStyleModule, ValidationModule, TextFilterModule, RowSelectionModule])
|
||||
ModuleRegistry.registerModules([
|
||||
ClientSideRowModelModule,
|
||||
ColumnAutoSizeModule,
|
||||
CellStyleModule,
|
||||
ValidationModule,
|
||||
TextFilterModule,
|
||||
TextEditorModule,
|
||||
NumberEditorModule,
|
||||
SelectEditorModule,
|
||||
UndoRedoEditModule,
|
||||
RowStyleModule,
|
||||
RenderApiModule,
|
||||
RowSelectionModule,
|
||||
ScrollApiModule
|
||||
])
|
||||
|
||||
const emit = defineEmits<{ rowSelected: [row: unknown] }>()
|
||||
|
||||
|
||||
const emit = defineEmits<{ rowSelected: [row: unknown]; cellValueChanged: [event: CellValueChangedEvent] }>()
|
||||
const props = withDefaults(defineProps<{ rows: unknown[]; columns: UiGridColumn[]; loading?: boolean; height?: string; rowSelection?: 'single' | 'multiple' | 'none'; showRowNumber?: boolean }>(), { loading: false, height: '32rem', rowSelection: 'single', showRowNumber: true })
|
||||
|
||||
const columnDefs = computed<ColDef[]>(() => {
|
||||
const mappedCols: ColDef[] = props.columns.map(column => ({
|
||||
field: column.field,
|
||||
headerName: column.header,
|
||||
width: column.width,
|
||||
minWidth: column.minWidth ?? 80,
|
||||
maxWidth: column.maxWidth,
|
||||
flex: column.flex ?? (column.width ? undefined : 1),
|
||||
|
||||
sortable: column.sortable ?? true,
|
||||
filter: column.filterable ?? true,
|
||||
filter: column.filterable ?? false,
|
||||
editable: column.editable ?? false,
|
||||
|
||||
valueFormatter: column.formatter ? params => column.formatter?.(params.value, params.data) ?? '' : undefined
|
||||
}))
|
||||
|
||||
@@ -32,6 +73,7 @@ const columnDefs = computed<ColDef[]>(() => {
|
||||
sortable: false,
|
||||
filter: false,
|
||||
resizable: false,
|
||||
editable: false,
|
||||
cellStyle: { textAlign: 'center', color: 'var(--ks-color-neutral-600)', fontSize: '11px' },
|
||||
headerClass: 'ks-row-number-header'
|
||||
}
|
||||
@@ -40,35 +82,204 @@ const columnDefs = computed<ColDef[]>(() => {
|
||||
|
||||
return mappedCols
|
||||
})
|
||||
const rowSelectionOptions = computed(() => props.rowSelection === 'none' ? undefined : ({ mode: props.rowSelection === 'multiple' ? 'multiRow' : 'singleRow' } as const))
|
||||
|
||||
const rowClassRules = computed(() => ({
|
||||
'ks-row-added': (params: RowClassParams) => {
|
||||
const data = params.data as Record<string, unknown> | undefined
|
||||
if (!data) return false
|
||||
return !!(data._isNew || (typeof data.code === 'string' && data.code.startsWith('NEW_')) || (typeof data.groupCode === 'string' && data.groupCode.startsWith('NEW_')))
|
||||
},
|
||||
'ks-row-modified': (params: RowClassParams) => {
|
||||
const data = params.data as Record<string, unknown> | undefined
|
||||
return data?._isModified === true
|
||||
}
|
||||
}))
|
||||
|
||||
const rowSelectionOptions = computed(() => {
|
||||
if (props.rowSelection === 'none') return undefined
|
||||
if (props.rowSelection === 'multiple') return { mode: 'multiRow' } as const
|
||||
return { mode: 'singleRow', checkboxes: false, enableClickSelection: true } as const
|
||||
})
|
||||
|
||||
function onRowClicked(event: RowClickedEvent): void {
|
||||
if (event.data) emit('rowSelected', event.data)
|
||||
}
|
||||
|
||||
function onCellValueChanged(event: CellValueChangedEvent): void {
|
||||
if (event.data) {
|
||||
(event.data as Record<string, unknown>)._isModified = true
|
||||
emit('cellValueChanged', event)
|
||||
}
|
||||
}
|
||||
|
||||
function onSortChanged(params: { api: { redrawRows: () => void; refreshCells: (options: { force: boolean }) => void } }): void {
|
||||
params.api.redrawRows()
|
||||
}
|
||||
function onGridReady(params: { api: { sizeColumnsToFit: () => void } }): void {
|
||||
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||
|
||||
const gridApi = ref<GridApi | null>(null)
|
||||
|
||||
function handleResize() {
|
||||
gridApi.value?.sizeColumnsToFit()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
function onGridReady(params: GridReadyEvent): void {
|
||||
gridApi.value = params.api
|
||||
params.api.sizeColumnsToFit()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function focusRow(rowIndex: number, colKey?: string): void {
|
||||
if (!gridApi.value) return
|
||||
const col = colKey || (props.columns[0]?.field ?? '')
|
||||
|
||||
const attempt = () => {
|
||||
if (!gridApi.value) return
|
||||
gridApi.value.ensureIndexVisible(rowIndex)
|
||||
if (col) {
|
||||
gridApi.value.setFocusedCell(rowIndex, col)
|
||||
gridApi.value.startEditingCell({ rowIndex, colKey: col })
|
||||
}
|
||||
}
|
||||
|
||||
attempt()
|
||||
setTimeout(attempt, 50)
|
||||
setTimeout(attempt, 150)
|
||||
}
|
||||
|
||||
|
||||
import type { NavigateToNextCellParams, CellPosition, CellEditingStoppedEvent } from 'ag-grid-community'
|
||||
|
||||
|
||||
|
||||
function navigateToNextCell(params: NavigateToNextCellParams): CellPosition | null {
|
||||
const previousCell = params.previousCellPosition
|
||||
const suggestedNextCell = params.nextCellPosition
|
||||
|
||||
if (params.key === 'Enter' || String(params.key) === '13') {
|
||||
const allColumns = params.api.getAllGridColumns()
|
||||
const editableCols = allColumns.filter(c => {
|
||||
const colDef = c.getColDef()
|
||||
return colDef.editable === true || typeof colDef.editable === 'function'
|
||||
})
|
||||
|
||||
if (editableCols.length > 0) {
|
||||
const currentColId = previousCell.column.getColId()
|
||||
const currentIndex = editableCols.findIndex(c => c.getColId() === currentColId)
|
||||
|
||||
if (currentIndex >= 0 && currentIndex < editableCols.length - 1) {
|
||||
const nextCol = editableCols[currentIndex + 1]
|
||||
const colId = nextCol.getColId()
|
||||
setTimeout(() => {
|
||||
params.api.startEditingCell({ rowIndex: previousCell.rowIndex, colKey: colId })
|
||||
}, 30)
|
||||
return {
|
||||
rowIndex: previousCell.rowIndex,
|
||||
column: nextCol,
|
||||
rowPinned: previousCell.rowPinned
|
||||
}
|
||||
} else {
|
||||
const nextRowIndex = previousCell.rowIndex + 1
|
||||
const firstCol = editableCols[0]
|
||||
const colId = firstCol.getColId()
|
||||
setTimeout(() => {
|
||||
params.api.startEditingCell({ rowIndex: nextRowIndex, colKey: colId })
|
||||
}, 30)
|
||||
return {
|
||||
rowIndex: nextRowIndex,
|
||||
column: firstCol,
|
||||
rowPinned: previousCell.rowPinned
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestedNextCell
|
||||
}
|
||||
|
||||
defineExpose({ focusRow, gridApi })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ks-grid" :aria-busy="props.loading">
|
||||
<div class="ks-grid" :style="{ height: props.height ?? '100%' }" :aria-busy="props.loading">
|
||||
<AgGridVue
|
||||
style="height: 100%; width: 100%"
|
||||
style="height: 100%; width: 100%; flex: 1; min-height: 0;"
|
||||
:theme="themeQuartz"
|
||||
:row-data="props.rows"
|
||||
:column-defs="columnDefs"
|
||||
:row-selection="rowSelectionOptions"
|
||||
:row-class-rules="rowClassRules"
|
||||
:enter-navigates-to-next-cell="true"
|
||||
:enter-navigates-to-next-cell-after-edit="true"
|
||||
:navigate-to-next-cell="navigateToNextCell"
|
||||
:stop-editing-when-cells-lose-focus="true"
|
||||
:loading="props.loading"
|
||||
overlay-no-rows-template="<div class='ks-ag-empty-overlay'><span style='font-size:2rem;'>📭</span><p style='margin-top:8px;font-weight:600;color:var(--ks-color-neutral-700);'>조회된 데이터가 없습니다</p><p style='font-size:12px;color:var(--ks-color-neutral-500);'>검색 조건을 변경하거나 신규 데이터를 수집하세요.</p></div>"
|
||||
@grid-ready="onGridReady"
|
||||
@first-data-rendered="onGridReady"
|
||||
@sort-changed="onSortChanged"
|
||||
@row-clicked="onRowClicked"
|
||||
@cell-value-changed="onCellValueChanged"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.ks-grid { flex: 1; height: 100%; width: 100%; min-height: 0; border: 1px solid var(--ks-color-neutral-200); border-radius: var(--ks-radius-md); overflow: hidden; background: #fff; }
|
||||
.ks-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
|
||||
border: 1px solid var(--ks-color-neutral-200);
|
||||
border-radius: var(--ks-radius-md);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.ag-row.ks-row-added) {
|
||||
background-color: #f0fdf4 !important;
|
||||
color: #15803d !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ag-row.ks-row-modified) {
|
||||
background-color: #fffbeb !important;
|
||||
color: #b45309 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ag-cell-editable) {
|
||||
background-color: #fafafa;
|
||||
cursor: text;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.ag-cell-editable:hover) {
|
||||
background-color: #f0f9ff !important;
|
||||
}
|
||||
|
||||
:deep(.ag-cell-inline-editing) {
|
||||
background-color: #ffffff !important;
|
||||
box-shadow: inset 0 0 0 2px var(--ks-color-action, #0284c7) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user