Skip to content

Commit ddb75fe

Browse files
Warn user of unsaved data output edits and optionally allow to save data before page navigation. #8916
1 parent 62e2d18 commit ddb75fe

File tree

8 files changed

+197
-108
lines changed

8 files changed

+197
-108
lines changed

web/pgadmin/static/js/custom_hooks.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,10 @@ export function useBeforeUnload({ enabled, isNewTab, beforeClose, closePanel })
286286

287287
return {forceClose};
288288
}
289+
290+
export function useLatestFunc(fn) {
291+
const fnRef = useRef(fn);
292+
fnRef.current = fn;
293+
294+
return useCallback(((...args) => fnRef.current(...args)), []);
295+
}

web/pgadmin/static/js/helpers/ModalProvider.jsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,14 @@ function alert(title, text, onOkClick, okLabel = gettext('OK')) {
8686
function confirm(title, text, onOkClick, onCancelClick, okLabel = gettext('Yes'), cancelLabel = gettext('No'), okIcon = 'default', modalId=null) {
8787
// bind the modal provider before calling
8888
this.showModal(title, (closeModal) => {
89-
const onCancelClickClose = () => {
90-
onCancelClick?.();
91-
closeModal();
92-
};
93-
9489
const onOkClickClose = () => {
9590
onOkClick?.();
9691
closeModal();
9792
};
9893
return (
99-
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={onCancelClickClose} okLabel={okLabel} cancelLabel={cancelLabel} okIcon={okIcon}/>
94+
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={closeModal} okLabel={okLabel} cancelLabel={cancelLabel} okIcon={okIcon}/>
10095
);
101-
}, {id: modalId});
96+
}, {id: modalId, onClose: onCancelClick});
10297
}
10398

10499
function confirmDelete(title, text, onDeleteClick, onCancelClick, deleteLabel = gettext('Delete'), cancelLabel = gettext('Cancel')) {

web/pgadmin/tools/sqleditor/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ def poll(trans_id):
10811081
if trans_status == TX_STATUS_INERROR and trans_obj.auto_rollback:
10821082
conn.execute_void("ROLLBACK;")
10831083

1084-
if is_thread_alive:
1084+
if conn.is_busy():
10851085
status = 'Busy'
10861086
messages = conn.messages()
10871087
if messages and len(messages) > 0:

web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const QUERY_TOOL_EVENTS = {
4949
LOAD_FILE_DONE: 'LOAD_FILE_DONE',
5050
SAVE_FILE: 'SAVE_FILE',
5151
SAVE_FILE_DONE: 'SAVE_FILE_DONE',
52+
SAVE_DATA_END: 'SAVE_DATA_END',
5253
QUERY_CHANGED: 'QUERY_CHANGED',
5354
API_ERROR: 'API_ERROR',
5455
TASK_START: 'TASK_START',

web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { GraphVisualiser } from './GraphVisualiser';
3131
import { usePgAdmin } from '../../../../../../static/js/PgAdminProvider';
3232
import pgAdmin from 'sources/pgadmin';
3333
import { connectServer, connectServerModal } from '../connectServer';
34+
import { useLatestFunc } from '../../../../../../static/js/custom_hooks';
3435

3536
const StyledBox = styled(Box)(({theme}) => ({
3637
display: 'flex',
@@ -846,7 +847,7 @@ export function ResultSet() {
846847
const modalId = MODAL_DIALOGS.QT_CONFIRMATIONS;
847848
// We'll use this track if any changes were saved.
848849
// It will help to decide whether results refresh is required or not on page change.
849-
const pageDataDirty = useRef(false);
850+
const pageDataOutOfSync = useRef(false);
850851

851852
const selectedCell = useRef([]);
852853
const selectedRange = useRef(null);
@@ -873,9 +874,10 @@ export function ResultSet() {
873874
// To use setLoaderText to the ResultSetUtils.
874875
rsu.current.setLoaderText = setLoaderText;
875876

876-
const isDataChanged = ()=>{
877-
return Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
878-
};
877+
const isDataChangedRef = useRef(false);
878+
useEffect(()=>{
879+
isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
880+
}, [dataChangeStore]);
879881

880882
const fireRowsColsCellChanged = ()=>{
881883
eventBus.fireEvent(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, selectedRows.size, selectedColumns.size, selectedRange.current, selectedCell.current?.length);
@@ -948,7 +950,7 @@ export function ResultSet() {
948950
});
949951
};
950952

951-
if(isDataChanged() && !refreshData) {
953+
if(isDataChangedRef.current && !refreshData) {
952954
queryToolCtx.modal.confirm(
953955
gettext('Unsaved changes'),
954956
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
@@ -1093,6 +1095,7 @@ export function ResultSet() {
10931095
setLoaderText(gettext('Fetching rows...'));
10941096
try {
10951097
res = await rsu.current.getWindowRows(fromRownum, toRownum);
1098+
resetSelectionAndChanges();
10961099
const newRows = rsu.current.processRows(res.data.data.result, columns);
10971100
setRows([...newRows]);
10981101
setQueryData((prev)=>({
@@ -1118,23 +1121,48 @@ export function ResultSet() {
11181121

11191122
useEffect(()=>{
11201123
let deregExecEnd;
1124+
let deregSaveDataDone;
11211125
const deregFetch = eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, (...args)=>{
1122-
if(pageDataDirty.current) {
1123-
deregExecEnd = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, (success)=>{
1124-
if(!success) return;
1125-
pageDataDirty.current = false;
1126+
const impl = ()=> {
1127+
if(pageDataOutOfSync.current) {
1128+
deregExecEnd = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, (success)=>{
1129+
if(!success) return;
1130+
pageDataOutOfSync.current = false;
1131+
fetchWindow(...args);
1132+
}, true);
1133+
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {refreshData: true});
1134+
// executionStartCallback(rsu.current.query, {refreshData: true});
1135+
} else {
1136+
pageDataOutOfSync.current = false;
11261137
fetchWindow(...args);
1127-
}, true);
1128-
executionStartCallback(rsu.current.query, {refreshData: true});
1129-
} else {
1130-
pageDataDirty.current = false;
1131-
fetchWindow(...args);
1138+
}
1139+
};
1140+
1141+
if(!isDataChangedRef.current) {
1142+
impl();
1143+
return;
11321144
}
1133-
resetSelectionAndChanges();
1145+
queryToolCtx.modal.showModal(gettext('Save data changes?'), (closeModal)=>(
1146+
<ConfirmSaveContent
1147+
closeModal={closeModal}
1148+
text={gettext('The data has changed. Do you want to save changes before moving to next page?')}
1149+
onDontSave={()=>{
1150+
impl();
1151+
}}
1152+
onSave={async ()=>{
1153+
deregSaveDataDone = eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_DATA_END, (success)=>{
1154+
if(!success) return;
1155+
impl();
1156+
}, true);
1157+
await triggerSaveData();
1158+
}}
1159+
/>
1160+
), {id: modalId});
11341161
});
11351162
return ()=>{
11361163
deregFetch();
11371164
deregExecEnd?.();
1165+
deregSaveDataDone?.();
11381166
};
11391167
}, [columns]);
11401168

@@ -1144,7 +1172,7 @@ export function ResultSet() {
11441172

11451173
const warnSaveDataClose = ()=>{
11461174
// No changes.
1147-
if(!isDataChanged() || !queryToolCtx.preferences?.sqleditor.prompt_save_data_changes) {
1175+
if(!isDataChangedRef.current || !queryToolCtx.preferences?.sqleditor.prompt_save_data_changes) {
11481176
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE);
11491177
return;
11501178
}
@@ -1172,8 +1200,9 @@ export function ResultSet() {
11721200
};
11731201
}, [dataChangeStore]);
11741202

1175-
const triggerSaveData = async ()=>{
1203+
const triggerSaveData = useLatestFunc(async ()=>{
11761204
if(!_.size(dataChangeStore.updated) && !_.size(dataChangeStore.added) && !_.size(dataChangeStore.deleted)) {
1205+
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_END, false);
11771206
return;
11781207
}
11791208
rsu.current.historyQuerySource = QuerySources.SAVE_DATA;
@@ -1210,7 +1239,7 @@ export function ResultSet() {
12101239
} catch {/* History errors should not bother others */}
12111240

12121241
if(!respData.data.status) {
1213-
pageDataDirty.current = false;
1242+
pageDataOutOfSync.current = false;
12141243
eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, respData.data.result);
12151244
pgAdmin.Browser.notifier.error(respData.data.result, 20000);
12161245
// If the transaction is not idle, notify the user that previous queries are not rolled back,
@@ -1220,10 +1249,11 @@ export function ResultSet() {
12201249
'still active; previous queries are unaffected.'));
12211250
}
12221251
setLoaderText(null);
1252+
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_END, false);
12231253
return;
12241254
}
12251255

1226-
pageDataDirty.current = true;
1256+
pageDataOutOfSync.current = true;
12271257
if(_.size(dataChangeStore.added)) {
12281258
// Update the rows in a grid after addition
12291259
respData.data.query_results.forEach((qr)=>{
@@ -1258,18 +1288,21 @@ export function ResultSet() {
12581288
resetSelectionAndChanges();
12591289
eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_CONNECTION_STATUS, respData.data.transaction_status);
12601290
eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, '');
1291+
setLoaderText(null);
1292+
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_END, true);
12611293
pgAdmin.Browser.notifier.success(gettext('Data saved successfully.'));
12621294
if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) {
12631295
pgAdmin.Browser.notifier.info(gettext('Auto-commit is off. You still need to commit changes to the database.'));
12641296
}
12651297
} catch (error) {
1266-
pageDataDirty.current = false;
1298+
pageDataOutOfSync.current = false;
12671299
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
12681300
checkTransaction: true,
12691301
});
1302+
setLoaderText(null);
1303+
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_END, false);
12701304
}
1271-
setLoaderText(null);
1272-
};
1305+
});
12731306

12741307
useEffect(()=>{
12751308
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_DATA, triggerSaveData);

web/pgadmin/tools/sqleditor/utils/start_running_query.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def execute(self, sql, trans_id, http_session, connect=False):
5454
can_edit = False
5555
can_filter = False
5656
notifies = None
57-
status = -1
57+
status = True
5858
result = None
5959
if transaction_object is not None and session_obj is not None:
6060
# set fetched row count to 0 as we are executing query again.
@@ -162,15 +162,15 @@ def asyn_exec_query(conn, sql, trans_obj, is_rollback_req,
162162
sql == 'ROLLBACK;'):
163163
conn.execute_void(sql)
164164
else:
165-
_, _ = conn.execute_async(
165+
status, _ = conn.execute_async(
166166
sql, server_cursor=trans_obj.server_cursor)
167-
# If the transaction aborted for some reason and
168-
# Auto RollBack is True then issue a rollback
169-
# to cleanup.
167+
# If the transaction aborted for some reason and
168+
# Auto RollBack is True then issue a rollback
169+
# to cleanup.
170170
if is_rollback_req:
171171
conn.execute_void("ROLLBACK;")
172172
except Exception as e:
173-
self.logger.error(e)
173+
self.logger.error(f"Error in background execution: {e}")
174174
return internal_server_error(errormsg=str(e))
175175

176176
_thread = QueryThread(target=asyn_exec_query,
@@ -181,8 +181,8 @@ def asyn_exec_query(conn, sql, trans_obj, is_rollback_req,
181181
_native_id = _thread.native_id if hasattr(_thread, 'native_id'
182182
) else _thread.ident
183183
trans_obj.set_thread_native_id(_native_id)
184-
StartRunningQuery.save_transaction_in_session(session_obj,
185-
trans_id, trans_obj)
184+
StartRunningQuery.save_transaction_in_session(session_obj, trans_id,
185+
trans_obj)
186186

187187
@staticmethod
188188
def is_begin_required_for_sql_query(trans_obj, conn, sql):

0 commit comments

Comments
 (0)