Warn user of unsaved data output edits before page navigation. #8916

This commit is contained in:
Aditya Toshniwal
2026-01-14 18:20:52 +05:30
committed by GitHub
parent e0ba1dd68e
commit 7c36eaba90
6 changed files with 53 additions and 29 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+2
View File
@@ -241,6 +241,8 @@ Pagination Options
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Edit Range* | Click to open the from and to rows range inputs to allow setting them. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Show Entire Range* | Click to get all the rows and set the from and to rows range | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Page No* | Enter the page no you want to jump to out of total shown next to this input | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *First Page* | Click to go to the first page. | |
+7
View File
@@ -286,3 +286,10 @@ export function useBeforeUnload({ enabled, isNewTab, beforeClose, closePanel })
return {forceClose};
}
export function useLatestFunc(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return useCallback(((...args) => fnRef.current(...args)), []);
}
@@ -86,19 +86,14 @@ function alert(title, text, onOkClick, okLabel = gettext('OK')) {
function confirm(title, text, onOkClick, onCancelClick, okLabel = gettext('Yes'), cancelLabel = gettext('No'), okIcon = 'default', modalId=null) {
// bind the modal provider before calling
this.showModal(title, (closeModal) => {
const onCancelClickClose = () => {
onCancelClick?.();
closeModal();
};
const onOkClickClose = () => {
onOkClick?.();
closeModal();
};
return (
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={onCancelClickClose} okLabel={okLabel} cancelLabel={cancelLabel} okIcon={okIcon}/>
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={closeModal} okLabel={okLabel} cancelLabel={cancelLabel} okIcon={okIcon}/>
);
}, {id: modalId});
}, {id: modalId, onClose: onCancelClick});
}
function confirmDelete(title, text, onDeleteClick, onCancelClick, deleteLabel = gettext('Delete'), cancelLabel = gettext('Cancel')) {
@@ -31,6 +31,7 @@ import { GraphVisualiser } from './GraphVisualiser';
import { usePgAdmin } from '../../../../../../static/js/PgAdminProvider';
import pgAdmin from 'sources/pgadmin';
import { connectServer, connectServerModal } from '../connectServer';
import { useLatestFunc } from '../../../../../../static/js/custom_hooks';
const StyledBox = styled(Box)(({theme}) => ({
display: 'flex',
@@ -846,7 +847,7 @@ export function ResultSet() {
const modalId = MODAL_DIALOGS.QT_CONFIRMATIONS;
// We'll use this track if any changes were saved.
// It will help to decide whether results refresh is required or not on page change.
const pageDataDirty = useRef(false);
const pageDataOutOfSync = useRef(false);
const selectedCell = useRef([]);
const selectedRange = useRef(null);
@@ -873,9 +874,10 @@ export function ResultSet() {
// To use setLoaderText to the ResultSetUtils.
rsu.current.setLoaderText = setLoaderText;
const isDataChanged = ()=>{
return Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
};
const isDataChangedRef = useRef(false);
useEffect(()=>{
isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
}, [dataChangeStore]);
const fireRowsColsCellChanged = ()=>{
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() {
});
};
if(isDataChanged() && !refreshData) {
if(isDataChangedRef.current && !refreshData) {
queryToolCtx.modal.confirm(
gettext('Unsaved changes'),
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() {
setLoaderText(gettext('Fetching rows...'));
try {
res = await rsu.current.getWindowRows(fromRownum, toRownum);
resetSelectionAndChanges();
const newRows = rsu.current.processRows(res.data.data.result, columns);
setRows([...newRows]);
setQueryData((prev)=>({
@@ -1119,18 +1122,33 @@ export function ResultSet() {
useEffect(()=>{
let deregExecEnd;
const deregFetch = eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, (...args)=>{
if(pageDataDirty.current) {
deregExecEnd = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, (success)=>{
if(!success) return;
pageDataDirty.current = false;
const impl = ()=> {
if(pageDataOutOfSync.current) {
deregExecEnd = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, (success)=>{
if(!success) return;
pageDataOutOfSync.current = false;
fetchWindow(...args);
}, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {refreshData: true});
// executionStartCallback(rsu.current.query, {refreshData: true});
} else {
pageDataOutOfSync.current = false;
fetchWindow(...args);
}, true);
executionStartCallback(rsu.current.query, {refreshData: true});
}
};
if(isDataChangedRef.current) {
queryToolCtx.modal.confirm(
gettext('Unsaved changes'),
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
impl,
function() {
/* Do nothing */
}
);
} else {
pageDataDirty.current = false;
fetchWindow(...args);
impl();
}
resetSelectionAndChanges();
});
return ()=>{
deregFetch();
@@ -1144,7 +1162,7 @@ export function ResultSet() {
const warnSaveDataClose = ()=>{
// No changes.
if(!isDataChanged() || !queryToolCtx.preferences?.sqleditor.prompt_save_data_changes) {
if(!isDataChangedRef.current || !queryToolCtx.preferences?.sqleditor.prompt_save_data_changes) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE);
return;
}
@@ -1172,7 +1190,7 @@ export function ResultSet() {
};
}, [dataChangeStore]);
const triggerSaveData = async ()=>{
const triggerSaveData = useLatestFunc(async ()=>{
if(!_.size(dataChangeStore.updated) && !_.size(dataChangeStore.added) && !_.size(dataChangeStore.deleted)) {
return;
}
@@ -1210,7 +1228,7 @@ export function ResultSet() {
} catch {/* History errors should not bother others */}
if(!respData.data.status) {
pageDataDirty.current = false;
pageDataOutOfSync.current = false;
eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, respData.data.result);
pgAdmin.Browser.notifier.error(respData.data.result, 20000);
// If the transaction is not idle, notify the user that previous queries are not rolled back,
@@ -1223,7 +1241,7 @@ export function ResultSet() {
return;
}
pageDataDirty.current = true;
pageDataOutOfSync.current = true;
if(_.size(dataChangeStore.added)) {
// Update the rows in a grid after addition
respData.data.query_results.forEach((qr)=>{
@@ -1258,18 +1276,19 @@ export function ResultSet() {
resetSelectionAndChanges();
eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_CONNECTION_STATUS, respData.data.transaction_status);
eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, '');
setLoaderText(null);
pgAdmin.Browser.notifier.success(gettext('Data saved successfully.'));
if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) {
pgAdmin.Browser.notifier.info(gettext('Auto-commit is off. You still need to commit changes to the database.'));
}
} catch (error) {
pageDataDirty.current = false;
pageDataOutOfSync.current = false;
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
checkTransaction: true,
});
setLoaderText(null);
}
setLoaderText(null);
};
});
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_DATA, triggerSaveData);
@@ -690,7 +690,8 @@ WHERE db.oid = {0}""".format(did))
if key == 'hostaddr' and self.use_ssh_tunnel:
continue
# Convert boolean connection parameters to integer for libpq compatibility
# Convert boolean connection parameters to integer for
# libpq compatibility
if key in ('sslcompression', 'sslsni'):
value = 1 if value else 0