🐛 fix(datagrid): 修复 Oracle DATE 编辑丢失时间

- Oracle-like DATE 字段按日期时间编辑,保留时分秒
- 普通 date 字段维持纯日期编辑行为
- 补充时间编辑器回归测试
This commit is contained in:
Syngnat
2026-06-11 18:27:44 +08:00
parent 5d4989f68f
commit 8006844b9f
3 changed files with 42 additions and 25 deletions

View File

@@ -777,6 +777,7 @@ interface EditableCellProps {
handleSave: (record: Item) => void;
focusCell?: (record: Item, dataIndex: string, title: React.ReactNode) => void;
columnType?: string;
dbType?: string;
inputCellPadding?: React.CSSProperties;
as?: any;
modifiedColumns?: Record<string, Set<string>>;
@@ -826,6 +827,7 @@ const areEditableCellPropsEqual = (prevProps: EditableCellProps, nextProps: Edit
if (prevProps.dataIndex !== nextProps.dataIndex) return false;
if (prevProps.title !== nextProps.title) return false;
if (prevProps.columnType !== nextProps.columnType) return false;
if (prevProps.dbType !== nextProps.dbType) return false;
if (prevProps.darkMode !== nextProps.darkMode) return false;
if (prevProps.as !== nextProps.as) return false;
if (prevProps.handleSave !== nextProps.handleSave) return false;
@@ -863,6 +865,7 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
handleSave,
focusCell,
columnType,
dbType,
inputCellPadding,
as: Component = 'td',
modifiedColumns,
@@ -950,7 +953,7 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
let childNode = children;
const pickerType = getTemporalPickerType(columnType);
const pickerType = getTemporalPickerType(columnType, dbType);
const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[dataIndex] || '')));
const isRowDeleted = deletedRowKeys && rowKeyStr && record?.[GONAVI_ROW_KEY] !== undefined
@@ -2334,7 +2337,7 @@ const DataGrid: React.FC<DataGridProps> = ({
if (value === undefined) return undefined;
const normalizedName = String(columnName || '').trim();
const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()];
const temporal = isTemporalColumnType(meta?.type);
const temporal = isTemporalColumnType(meta?.type, dbType);
if (!temporal) {
return value;
@@ -2355,7 +2358,7 @@ const DataGrid: React.FC<DataGridProps> = ({
return value;
},
[columnMetaMap, columnMetaMapByLowerName]
[columnMetaMap, columnMetaMapByLowerName, dbType]
);
const openForeignKeyTarget = useCallback((target: ForeignKeyTarget) => {
@@ -4376,7 +4379,7 @@ const DataGrid: React.FC<DataGridProps> = ({
}
const columnType = (columnMetaMap[dataIndex] || columnMetaMapByLowerName[dataIndex.toLowerCase()])?.type;
const pickerType = getTemporalPickerType(columnType);
const pickerType = getTemporalPickerType(columnType, dbType);
const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(raw || '')));
const fieldName = getCellFieldName(record, dataIndex);
if (isDateTimeField) {
@@ -4391,7 +4394,7 @@ const DataGrid: React.FC<DataGridProps> = ({
title,
columnType,
});
}, [canModifyData, columnMetaMap, columnMetaMapByLowerName, form, openCellEditor, rowKeyStr]);
}, [canModifyData, columnMetaMap, columnMetaMapByLowerName, dbType, form, openCellEditor, rowKeyStr]);
const handleVirtualCellActivate = useCallback((record: Item, dataIndex: string, title: React.ReactNode) => {
if (!canModifyData) return;
@@ -4521,7 +4524,7 @@ const DataGrid: React.FC<DataGridProps> = ({
return;
}
const pickerType = getTemporalPickerType(editingCell.columnType);
const pickerType = getTemporalPickerType(editingCell.columnType, dbType);
const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[editingCell.dataIndex] || '')));
const fieldName = getCellFieldName(record, editingCell.dataIndex);
try {
@@ -4540,7 +4543,7 @@ const DataGrid: React.FC<DataGridProps> = ({
closeVirtualInlineEditor();
}
}
}, [closeVirtualInlineEditor, form, handleCellSave, virtualEditingCell]);
}, [closeVirtualInlineEditor, dbType, form, handleCellSave, virtualEditingCell]);
const pageFindMatches = useMemo(() => collectDataGridFindMatches(
mergedDisplayData,
@@ -4662,7 +4665,7 @@ const DataGrid: React.FC<DataGridProps> = ({
displayMap[col] = toFormText(displayVal);
// 日期时间类型: 将字符串值转为 dayjs 对象供 DatePicker 使用
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
const rowPickerType = getTemporalPickerType(colMeta?.type);
const rowPickerType = getTemporalPickerType(colMeta?.type, dbType);
if (rowPickerType && displayVal !== null && displayVal !== undefined) {
const dVal = parseToDayjs(displayVal, rowPickerType);
formMap[col] = dVal;
@@ -4679,7 +4682,7 @@ const DataGrid: React.FC<DataGridProps> = ({
nullCols,
formValues: formMap,
});
}, [canModifyData, mergedDisplayData, data, addedRows, visibleColumnNames, rowKeyStr, columnMetaMap, columnMetaMapByLowerName, openRowEditor]);
}, [canModifyData, mergedDisplayData, data, addedRows, visibleColumnNames, rowKeyStr, columnMetaMap, columnMetaMapByLowerName, dbType, openRowEditor]);
const openCurrentViewRowEditor = useCallback(() => {
if (!canModifyData) return;
@@ -4844,7 +4847,7 @@ const DataGrid: React.FC<DataGridProps> = ({
if (!isWritableResultColumn(col, effectiveEditLocator)) return;
if (val && dayjs.isDayjs(val)) {
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
const rowPickerType = getTemporalPickerType(colMeta?.type);
const rowPickerType = getTemporalPickerType(colMeta?.type, dbType);
convertedValues[col] = formatFromDayjs(val as dayjs.Dayjs, rowPickerType);
} else {
convertedValues[col] = val;
@@ -4863,7 +4866,7 @@ const DataGrid: React.FC<DataGridProps> = ({
// 日期时间类型: 将 dayjs 对象转回格式化字符串
if (nextVal && dayjs.isDayjs(nextVal)) {
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
const rowPickerType = getTemporalPickerType(colMeta?.type);
const rowPickerType = getTemporalPickerType(colMeta?.type, dbType);
nextVal = formatFromDayjs(nextVal as dayjs.Dayjs, rowPickerType);
}
const baseVal = baseRawMap[col];
@@ -4878,7 +4881,7 @@ const DataGrid: React.FC<DataGridProps> = ({
});
closeRowEditor();
}, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor, effectiveEditLocator, columnMetaMap, columnMetaMapByLowerName]);
}, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor, effectiveEditLocator, columnMetaMap, columnMetaMapByLowerName, dbType]);
const enableVirtual = isTableSurfaceActive;
@@ -4994,6 +4997,7 @@ const DataGrid: React.FC<DataGridProps> = ({
cellProps.handleSave = handleCellSave;
cellProps.focusCell = openCellEditor;
cellProps.columnType = displayColumnTypeMap[dataIndex];
cellProps.dbType = dbType;
cellProps.inputCellPadding = inputCellPadding;
cellProps.modifiedColumns = modifiedColumns;
cellProps.rowKeyStr = rowKeyStr;
@@ -5024,7 +5028,7 @@ const DataGrid: React.FC<DataGridProps> = ({
: undefined;
const shouldUsePlainVirtualContent = isV2Ui && !modifiedStyle;
if (enableVirtual && enableInlineEditableCell) {
const pickerType = getTemporalPickerType(columnType);
const pickerType = getTemporalPickerType(columnType, dbType);
const isDateTimeField = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(record?.[dataIndex] || '')));
const virtualCellStyle = modifiedStyle ? { ...virtualCellWrapperStyle, ...modifiedStyle } : virtualCellWrapperStyle;
const virtualEditable = !!col.editable && !rowDeletedForRender;
@@ -5138,7 +5142,7 @@ const DataGrid: React.FC<DataGridProps> = ({
return originalRenderContent;
}
};
}), [columns, useInlineEditableBodyCell, enableInlineEditableCell, enableVirtual, handleCellSave, openCellEditor, handleVirtualCellActivate, handleSharedCellContextMenu, displayColumnTypeMap, inputCellPadding, virtualCellWrapperStyle, modifiedColumns, rowKeyStr, deletedRowKeys, darkMode, virtualEditingCell, form, saveVirtualInlineEditor, lockVirtualInlineTableScroll, closeVirtualInlineEditor, updateFocusedCell]);
}), [columns, useInlineEditableBodyCell, enableInlineEditableCell, enableVirtual, handleCellSave, openCellEditor, handleVirtualCellActivate, handleSharedCellContextMenu, displayColumnTypeMap, dbType, inputCellPadding, virtualCellWrapperStyle, modifiedColumns, rowKeyStr, deletedRowKeys, darkMode, virtualEditingCell, form, saveVirtualInlineEditor, lockVirtualInlineTableScroll, closeVirtualInlineEditor, updateFocusedCell]);
const handleAddRow = () => {
const newKey = `new-${Date.now()}`;
@@ -7439,7 +7443,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const isJson = looksLikeJsonText(sample);
const useTextArea = isJson || sample.includes('\n') || sample.length >= 160;
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
const pickerType = getTemporalPickerType(colMeta?.type);
const pickerType = getTemporalPickerType(colMeta?.type, dbType);
const isTemporalValue = !!pickerType && !(/^0{4}-0{2}-0{2}/.test(String(sample || '')));
const isWritable = isWritableResultColumn(col, effectiveEditLocator);
return {
@@ -7453,7 +7457,7 @@ const DataGrid: React.FC<DataGridProps> = ({
isWritable,
};
})
), [displayColumnNames, columnMetaMap, columnMetaMapByLowerName, effectiveEditLocator, rowEditorOpen, rowEditorRowKey]);
), [displayColumnNames, columnMetaMap, columnMetaMapByLowerName, dbType, effectiveEditLocator, rowEditorOpen, rowEditorRowKey]);
const handleRefreshGrid = useCallback(() => {
clearAutoCommitTimer();

View File

@@ -1,10 +1,26 @@
import dayjs from 'dayjs';
import { describe, expect, it } from 'vitest';
import { resolveTemporalEditorSaveValue } from './dataGridTemporal';
import { getTemporalPickerType, resolveTemporalEditorSaveValue } from './dataGridTemporal';
describe('dataGridTemporal helpers', () => {
it('prefers the picker selected date when form store has not caught up yet', () => {
expect(resolveTemporalEditorSaveValue(undefined, dayjs('2026-04-12'), 'date')).toBe('2026-04-12');
});
it('treats Oracle DATE as datetime because the type stores time to seconds', () => {
const pickerType = getTemporalPickerType('DATE', 'oracle');
expect(pickerType).toBe('datetime');
expect(resolveTemporalEditorSaveValue(undefined, dayjs('2026-06-11 19:42:13'), pickerType))
.toBe('2026-06-11 19:42:13');
});
it('keeps non Oracle DATE columns as date-only values', () => {
const pickerType = getTemporalPickerType('date', 'mysql');
expect(pickerType).toBe('date');
expect(resolveTemporalEditorSaveValue(undefined, dayjs('2026-06-11 19:42:13'), pickerType))
.toBe('2026-06-11');
});
});

View File

@@ -1,4 +1,5 @@
import dayjs from 'dayjs';
import { isOracleLikeDialect } from '../utils/sqlDialect';
export type TemporalPickerType = 'datetime' | 'date' | 'time' | 'year' | null;
@@ -9,20 +10,16 @@ export const TEMPORAL_FORMATS: Record<string, string> = {
year: 'YYYY',
};
export const isTemporalColumnType = (columnType?: string): boolean => {
const raw = String(columnType || '').trim().toLowerCase();
if (!raw) return false;
if (raw.includes('datetime') || raw.includes('timestamp')) return true;
const base = raw.split(/[ (]/)[0];
return base === 'date' || base === 'time' || base === 'year';
export const isTemporalColumnType = (columnType?: string, dbType?: string): boolean => {
return !!getTemporalPickerType(columnType, dbType);
};
export const getTemporalPickerType = (columnType?: string): TemporalPickerType => {
export const getTemporalPickerType = (columnType?: string, dbType?: string): TemporalPickerType => {
const raw = String(columnType || '').trim().toLowerCase();
if (!raw) return null;
if (raw.includes('datetime') || raw.includes('timestamp')) return 'datetime';
const base = raw.split(/[ (]/)[0];
if (base === 'date') return 'date';
if (base === 'date') return isOracleLikeDialect(String(dbType || '')) ? 'datetime' : 'date';
if (base === 'time') return 'time';
if (base === 'year') return 'year';
return null;