mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
✨ feat(query-editor): 对齐 DBeaver 风格事务提交模式
This commit is contained in:
@@ -45,7 +45,7 @@ const storeState = vi.hoisted(() => ({
|
||||
setQueryOptions: vi.fn(),
|
||||
sqlEditorTransactionOptions: {
|
||||
commitMode: 'manual' as 'manual' | 'auto',
|
||||
autoCommitDelayMs: 5000,
|
||||
autoCommitDelayMs: 0,
|
||||
},
|
||||
setSqlEditorTransactionOptions: vi.fn(),
|
||||
sqlEditorPendingTransactions: {} as Record<string, unknown>,
|
||||
@@ -461,7 +461,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
};
|
||||
storeState.sqlEditorTransactionOptions = {
|
||||
commitMode: 'manual',
|
||||
autoCommitDelayMs: 5000,
|
||||
autoCommitDelayMs: 0,
|
||||
};
|
||||
storeState.shortcutOptions = {
|
||||
runQuery: {
|
||||
@@ -2347,6 +2347,53 @@ describe('QueryEditor external SQL save', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('supports DBeaver-style immediate auto-commit for SQL editor DML transactions', async () => {
|
||||
vi.useFakeTimers();
|
||||
storeState.sqlEditorTransactionOptions = {
|
||||
commitMode: 'auto',
|
||||
autoCommitDelayMs: 0,
|
||||
};
|
||||
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transactionId: 'tx-auto-now',
|
||||
transactionPending: true,
|
||||
data: [
|
||||
{ columns: ['affectedRows'], rows: [{ affectedRows: 1 }], statementIndex: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ query: "UPDATE users SET active = 0 WHERE id = 1" })} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalled();
|
||||
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
|
||||
expect(textContent(renderer!.root)).toContain('事务执行成功,正在自动提交');
|
||||
expect(backendApp.DBCommitTransaction).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBCommitTransaction).toHaveBeenCalledWith('tx-auto-now');
|
||||
expect(textContent(renderer!.root)).not.toContain('事务执行成功,正在自动提交');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('automatically appends hidden primary key locator columns for editable query results', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'ORCLPDB1';
|
||||
@@ -3569,12 +3616,14 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(source).toContain('QueryEditorTransactionSettings');
|
||||
expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-mode-select');
|
||||
expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-delay-select');
|
||||
expect(transactionSettingsSource).toContain('这里仅选择事务执行成功后的 COMMIT 时机');
|
||||
expect(transactionSettingsSource).toContain("label: '提交:手动 COMMIT'");
|
||||
expect(transactionSettingsSource).toContain("label: '提交:自动 COMMIT'");
|
||||
expect(transactionSettingsSource).toContain('参考 DBeaver');
|
||||
expect(transactionSettingsSource).toContain("label: 'Manual Commit'");
|
||||
expect(transactionSettingsSource).toContain("label: 'Auto-commit'");
|
||||
expect(transactionSettingsSource).toContain("label: '立即'");
|
||||
expect(source).toContain('QueryEditorTransactionToolbar');
|
||||
expect(transactionToolbarSource).toContain("className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}");
|
||||
expect(transactionToolbarSource).toContain('事务待提交');
|
||||
expect(transactionToolbarSource).toContain('事务执行成功,正在自动提交');
|
||||
expect(transactionToolbarSource).toContain('onFinish');
|
||||
expect(source).toContain('gn-v2-query-toolbar-action-group');
|
||||
expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 160 }}');
|
||||
|
||||
@@ -2086,7 +2086,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const sqlEditorCommitMode = sqlEditorTransactionOptions?.commitMode === 'auto' ? 'auto' : 'manual';
|
||||
const sqlEditorAutoCommitDelayMs = SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.some((item) => item.value === sqlEditorTransactionOptions?.autoCommitDelayMs)
|
||||
? Number(sqlEditorTransactionOptions?.autoCommitDelayMs)
|
||||
: 5000;
|
||||
: 0;
|
||||
const clearSqlEditorAutoCommitTimer = useCallback(() => {
|
||||
if (sqlEditorAutoCommitTimerRef.current) {
|
||||
clearTimeout(sqlEditorAutoCommitTimerRef.current);
|
||||
@@ -2137,12 +2137,22 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}, [clearSqlEditorAutoCommitTimer, updatePendingSqlTransaction]);
|
||||
const activatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction) => {
|
||||
clearSqlEditorAutoCommitTimer();
|
||||
const dueAt = transaction.commitMode === 'auto' ? Date.now() + transaction.autoCommitDelayMs : null;
|
||||
const nextTransaction = { ...transaction, autoCommitDueAt: dueAt };
|
||||
const autoCommitDelayMs = Math.max(0, Number(transaction.autoCommitDelayMs) || 0);
|
||||
const dueAt = transaction.commitMode === 'auto' ? Date.now() + autoCommitDelayMs : null;
|
||||
const nextTransaction = { ...transaction, autoCommitDelayMs, autoCommitDueAt: dueAt };
|
||||
updatePendingSqlTransaction(nextTransaction);
|
||||
if (nextTransaction.commitMode !== 'auto' || !dueAt) {
|
||||
return;
|
||||
}
|
||||
if (autoCommitDelayMs === 0) {
|
||||
setSqlEditorAutoCommitRemainingSeconds(0);
|
||||
sqlEditorAutoCommitTimerRef.current = setTimeout(() => {
|
||||
sqlEditorAutoCommitTimerRef.current = null;
|
||||
setSqlEditorAutoCommitRemainingSeconds(null);
|
||||
void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
const updateRemaining = () => {
|
||||
setSqlEditorAutoCommitRemainingSeconds(Math.max(1, Math.ceil((dueAt - Date.now()) / 1000)));
|
||||
};
|
||||
@@ -2156,7 +2166,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
setSqlEditorAutoCommitRemainingSeconds(null);
|
||||
void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id);
|
||||
}, nextTransaction.autoCommitDelayMs);
|
||||
}, autoCommitDelayMs);
|
||||
}, [clearSqlEditorAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -5298,7 +5308,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
isV2Ui={isV2Ui}
|
||||
commitMode={sqlEditorCommitMode}
|
||||
autoCommitDelayMs={sqlEditorAutoCommitDelayMs}
|
||||
onCommitModeChange={(mode) => setSqlEditorTransactionOptions({ commitMode: mode })}
|
||||
onCommitModeChange={(mode) => setSqlEditorTransactionOptions(
|
||||
mode === 'auto'
|
||||
? { commitMode: mode, autoCommitDelayMs: 0 }
|
||||
: { commitMode: mode },
|
||||
)}
|
||||
onAutoCommitDelayMsChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: delayMs })}
|
||||
/>
|
||||
{pendingSqlTransaction && sqlEditorTransactionToolbar}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { Select, Tooltip } from 'antd';
|
||||
export type SqlEditorCommitMode = 'manual' | 'auto';
|
||||
|
||||
export const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = [
|
||||
{ value: 3000, label: '3 秒' },
|
||||
{ value: 5000, label: '5 秒' },
|
||||
{ value: 10000, label: '10 秒' },
|
||||
{ value: 30000, label: '30 秒' },
|
||||
{ value: 0, label: '立即' },
|
||||
{ value: 3000, label: '3 秒后' },
|
||||
{ value: 5000, label: '5 秒后' },
|
||||
{ value: 10000, label: '10 秒后' },
|
||||
{ value: 30000, label: '30 秒后' },
|
||||
];
|
||||
|
||||
type QueryEditorTransactionSettingsProps = {
|
||||
@@ -26,15 +27,15 @@ const QueryEditorTransactionSettings: React.FC<QueryEditorTransactionSettingsPro
|
||||
onAutoCommitDelayMsChange,
|
||||
}) => (
|
||||
<>
|
||||
<Tooltip title="SQL 编辑器执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时固定开启受管事务;这里仅选择事务执行成功后的 COMMIT 时机。">
|
||||
<Tooltip title="参考 DBeaver:SQL 编辑器执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时先进入 GoNavi 托管事务;Manual Commit 需要手动提交/回滚,Auto-commit 在执行成功后自动 COMMIT。">
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-mode-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 160 }}
|
||||
value={commitMode}
|
||||
onChange={(mode) => onCommitModeChange(mode === 'auto' ? 'auto' : 'manual')}
|
||||
options={[
|
||||
{ label: '提交:手动 COMMIT', value: 'manual' },
|
||||
{ label: '提交:自动 COMMIT', value: 'auto' },
|
||||
{ label: 'Manual Commit', value: 'manual' },
|
||||
{ label: 'Auto-commit', value: 'auto' },
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -28,6 +28,12 @@ const QueryEditorTransactionToolbar: React.FC<QueryEditorTransactionToolbarProps
|
||||
return null;
|
||||
}
|
||||
|
||||
const statusText = transaction.commitMode === 'auto'
|
||||
? autoCommitRemainingSeconds !== null && autoCommitRemainingSeconds > 0
|
||||
? `事务待提交,${autoCommitRemainingSeconds}s 后自动提交`
|
||||
: '事务执行成功,正在自动提交'
|
||||
: '事务待提交';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}
|
||||
@@ -40,9 +46,7 @@ const QueryEditorTransactionToolbar: React.FC<QueryEditorTransactionToolbarProps
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: darkMode ? '#d4d4d4' : '#666' }}>
|
||||
{transaction.commitMode === 'auto' && autoCommitRemainingSeconds !== null
|
||||
? `事务待提交,${autoCommitRemainingSeconds}s 后自动提交`
|
||||
: '事务待提交'}
|
||||
{statusText}
|
||||
</span>
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -1637,7 +1637,7 @@ const sanitizeQueryOptions = (value: unknown): QueryOptions => {
|
||||
};
|
||||
|
||||
const DATA_EDIT_AUTO_COMMIT_DELAY_OPTIONS = new Set([3000, 5000, 10000, 30000]);
|
||||
const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = new Set([3000, 5000, 10000, 30000]);
|
||||
const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = new Set([0, 3000, 5000, 10000, 30000]);
|
||||
|
||||
const sanitizeDataEditTransactionOptions = (
|
||||
value: unknown,
|
||||
@@ -1667,7 +1667,7 @@ const sanitizeSqlEditorTransactionOptions = (
|
||||
commitMode: raw.commitMode === "auto" ? "auto" : "manual",
|
||||
autoCommitDelayMs: SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.has(autoCommitDelayMs)
|
||||
? autoCommitDelayMs
|
||||
: 5000,
|
||||
: 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2063,7 +2063,7 @@ export const useStore = create<AppState>()(
|
||||
},
|
||||
sqlEditorTransactionOptions: {
|
||||
commitMode: "manual",
|
||||
autoCommitDelayMs: 5000,
|
||||
autoCommitDelayMs: 0,
|
||||
},
|
||||
sqlEditorPendingTransactions: {},
|
||||
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
|
||||
|
||||
Reference in New Issue
Block a user