mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 00:39:41 +08:00
🐛 fix(query-editor/data-grid): 修复只读查询结果右键菜单失效及提交事务后数据丢失
- 右键菜单修复:移除 handleContextMenu 的 editable 守卫,只读模式也能弹出右键菜单 - 非编辑单元格绑定:EditableCell 非编辑模式增加 onContextMenu 包装,确保右键事件触发 - mergedColumns 统一:所有列通过 onCell 绑定 onContextMenu,不再跳过非 editable 列 - 表名正则增强:支持多行 SQL 和 schema.table 写法,复杂 SELECT 也能提取表名获得编辑能力 - 精准重查询:新增 handleReloadResult 函数,提交事务后只用当前结果集 SQL 重查,避免整个编辑器 SQL 二次处理导致数据丢失 - refs #267
This commit is contained in:
@@ -567,12 +567,10 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
if (!editable) return;
|
||||
if (!cellContextMenuContext) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // 阻止冒泡到行级菜单
|
||||
if (cellContextMenuContext) {
|
||||
cellContextMenuContext.showMenu(e, record, dataIndex, title);
|
||||
}
|
||||
cellContextMenuContext.showMenu(e, record, dataIndex, title);
|
||||
};
|
||||
|
||||
let childNode = children;
|
||||
@@ -611,6 +609,13 @@ const EditableCell: React.FC<EditableCellProps> = React.memo(({
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
} else if (cellContextMenuContext) {
|
||||
// 非编辑模式(只读查询结果)也绑定右键菜单,支持复制为 INSERT/JSON/CSV 等操作
|
||||
childNode = (
|
||||
<div onContextMenu={handleContextMenu} style={{ minHeight: 20 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
@@ -3081,8 +3086,8 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}, [displayColumnNames, columnWidths, sortInfo, handleResizeStart, canModifyData, onSort, renderColumnTitle]);
|
||||
|
||||
const mergedColumns = useMemo(() => columns.map((col): ColumnType<any> => {
|
||||
if (!col.editable) return col as ColumnType<any>;
|
||||
const dataIndex = String(col.dataIndex);
|
||||
// 即使不可编辑,也需要通过 onCell/render 绑定右键菜单
|
||||
return {
|
||||
...col,
|
||||
onCell: (record: Item) => {
|
||||
@@ -3092,7 +3097,16 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
'data-col-name': dataIndex,
|
||||
};
|
||||
|
||||
if (!enableInlineEditableCell) {
|
||||
if (col.editable && enableInlineEditableCell) {
|
||||
// 可编辑模式(非虚拟):传递给 EditableCell 的 props
|
||||
cellProps.record = record;
|
||||
cellProps.editable = col.editable;
|
||||
cellProps.dataIndex = col.dataIndex;
|
||||
cellProps.title = dataIndex;
|
||||
cellProps.handleSave = handleCellSave;
|
||||
cellProps.focusCell = openCellEditor;
|
||||
} else if (col.editable && !enableInlineEditableCell) {
|
||||
// 可编辑但非 inline(虚拟模式下):双击和右键通过 onCell 绑定
|
||||
cellProps.onDoubleClick = () => handleVirtualCellActivate(record, dataIndex, dataIndex);
|
||||
cellProps.onContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -3100,12 +3114,12 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
showCellContextMenu(e, record, dataIndex, dataIndex);
|
||||
};
|
||||
} else {
|
||||
cellProps.record = record;
|
||||
cellProps.editable = col.editable;
|
||||
cellProps.dataIndex = col.dataIndex;
|
||||
cellProps.title = dataIndex;
|
||||
cellProps.handleSave = handleCellSave;
|
||||
cellProps.focusCell = openCellEditor;
|
||||
// 不可编辑(只读查询结果):只绑定右键菜单
|
||||
cellProps.onContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCellContextMenu(e, record, dataIndex, dataIndex);
|
||||
};
|
||||
}
|
||||
return cellProps;
|
||||
},
|
||||
|
||||
@@ -1313,6 +1313,72 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
return selected;
|
||||
};
|
||||
|
||||
// 精准重查询单个结果集(提交事务 / 刷新按钮使用),不会重跑整个编辑器 SQL
|
||||
const handleReloadResult = async (resultKey: string, sql: string) => {
|
||||
if (!sql?.trim() || !currentDb) return;
|
||||
const conn = connections.find(c => c.id === currentConnectionId);
|
||||
if (!conn) return;
|
||||
|
||||
const config = {
|
||||
...conn.config,
|
||||
port: Number(conn.config.port),
|
||||
password: conn.config.password || "",
|
||||
database: conn.config.database || "",
|
||||
useSSH: conn.config.useSSH || false,
|
||||
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
|
||||
};
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
// 使用 DBQueryMulti 保持和首次查询一致的后端路径
|
||||
let queryId: string;
|
||||
try {
|
||||
queryId = await GenerateQueryID();
|
||||
} catch {
|
||||
queryId = 'reload-' + Date.now();
|
||||
}
|
||||
const res = await DBQueryMulti(config as any, currentDb, sql, queryId);
|
||||
if (!res?.success) {
|
||||
message.error('刷新失败: ' + (res?.message || '未知错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个结果集(单条 SQL 只有一个结果集)
|
||||
const resultSetDataArray = Array.isArray(res.data) ? (res.data as any[]) : [];
|
||||
if (resultSetDataArray.length === 0) return;
|
||||
const rsData = resultSetDataArray[0];
|
||||
const isAffectedResult = Array.isArray(rsData.rows) && rsData.rows.length === 1
|
||||
&& rsData.columns && rsData.columns.length === 1
|
||||
&& rsData.columns[0] === 'affectedRows';
|
||||
if (isAffectedResult) return; // 不应该出现,但保险起见
|
||||
|
||||
let rows = Array.isArray(rsData.rows) ? rsData.rows : [];
|
||||
const maxRows = Number(queryOptions?.maxRows) || 0;
|
||||
let truncated = false;
|
||||
if (Number.isFinite(maxRows) && maxRows > 0 && rows.length > maxRows) {
|
||||
truncated = true;
|
||||
rows = rows.slice(0, maxRows);
|
||||
}
|
||||
const cols = (rsData.columns && rsData.columns.length > 0)
|
||||
? rsData.columns
|
||||
: (rows.length > 0 ? Object.keys(rows[0]) : []);
|
||||
rows.forEach((row: any, i: number) => {
|
||||
if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = i;
|
||||
});
|
||||
|
||||
// 只更新匹配的结果集的 rows 和 columns,保留 tableName/pkColumns/readOnly 等元数据
|
||||
setResultSets(prev => prev.map(rs =>
|
||||
rs.key === resultKey
|
||||
? { ...rs, rows, columns: cols, truncated }
|
||||
: rs
|
||||
));
|
||||
} catch (err: any) {
|
||||
message.error('刷新失败: ' + (err?.message || '未知错误'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRun = async () => {
|
||||
const currentQuery = getCurrentQuery();
|
||||
if (!currentQuery.trim()) return;
|
||||
@@ -1601,7 +1667,8 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
|
||||
let simpleTableName: string | undefined = undefined;
|
||||
if (rawStatement) {
|
||||
const tableMatch = rawStatement.match(/^\s*SELECT\s+\*\s+FROM\s+[`"]?(\w+)[`"]?\s*(?:WHERE.*)?(?:ORDER BY.*)?(?:LIMIT.*)?$/i);
|
||||
// 支持多行 SQL:SELECT * FROM [schema.]table [WHERE...] [ORDER BY...] [LIMIT...] 等
|
||||
const tableMatch = rawStatement.match(/^\s*SELECT\s+\*\s+FROM\s+(?:[\w`"]+\.)?[`"]?(\w+)[`"]?\s*(?:$|[\s;])/im);
|
||||
if (tableMatch) {
|
||||
simpleTableName = tableMatch[1];
|
||||
if (!forceReadOnlyResult) {
|
||||
@@ -2060,7 +2127,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
dbName={currentDb}
|
||||
connectionId={currentConnectionId}
|
||||
pkColumns={rs.pkColumns}
|
||||
onReload={handleRun}
|
||||
onReload={() => handleReloadResult(rs.key, rs.sql)}
|
||||
readOnly={rs.readOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user