Added support to automatically open a file after it is downloaded in the desktop mode. #4194

This commit is contained in:
Pravesh Sharma
2025-03-10 11:38:13 +05:30
committed by GitHub
parent df5dced926
commit 73b437c0f5
14 changed files with 107 additions and 59 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

+18
View File
@@ -273,6 +273,24 @@ The Miscellaneous Node
Expand the *Miscellaneous* node to specify miscellaneous display preferences.
.. image:: images/preferences_misc_file_downloads.png
:alt: Preferences dialog file downloads section
:align: center
Use the fields on the *File Downloads* panel to manage file downloads related preferences.
* When the *Automatically open downloaded files?* switch is set to *True*
the downloaded file will automatically open in the system's default
application associated with that file type.
* When the *Prompt for the download location?* switch is set to *True*
a prompt will appear after clicking the download button, allowing you
to choose the download location.
**Note:** File Downloads related settings are applicable and visible only in desktop mode.
Use the fields on the *User Interface* panel to set the user interface related preferences.
.. image:: images/preferences_misc_user_interface.png
:alt: Preferences dialog user interface section
:align: center
+1
View File
@@ -18,6 +18,7 @@
"dependencies": {
"axios": "^1.8.1",
"electron-context-menu": "^4.0.5",
"electron-dl": "^4.0.0",
"electron-store": "^10.0.0"
}
}
+24
View File
@@ -16,6 +16,7 @@ import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { setupMenu } from './menu.js';
import contextMenu from 'electron-context-menu';
import { CancelError, download } from 'electron-dl';
const configStore = new Store({
defaults: {
@@ -126,6 +127,28 @@ function reloadApp() {
currWin.webContents.reload();
}
async function desktopFileDownload(payload) {
const currWin = BrowserWindow.getFocusedWindow();
try {
await download(currWin, payload.downloadUrl, {
filename: payload.fileName,
saveAs: payload.prompt_for_download_location,
onProgress: (progress) => {
currWin.webContents.send('download-progress', progress);
},
onCompleted: (item) => {
currWin.webContents.send('download-complete', item);
if (payload.automatically_open_downloaded_file)
shell.openPath(item.path);
},
});
} catch (error) {
if (!(error instanceof CancelError)) {
misc.writeServerLog(error);
}
}
}
// This functions is used to start the pgAdmin4 server by spawning a
// separate process.
function startDesktopMode() {
@@ -369,6 +392,7 @@ ipcMain.on('log', (text) => ()=>{
misc.writeServerLog(text);
});
ipcMain.on('reloadApp', reloadApp);
ipcMain.on('onFileDownload', (_, payload) => desktopFileDownload(payload));
ipcMain.handle('checkPortAvailable', async (_e, fixedPort)=>{
try {
await misc.getAvailablePort(fixedPort);
+1
View File
@@ -24,4 +24,5 @@ contextBridge.exposeInMainWorld('electronUI', {
showSaveDialog: (options) => ipcRenderer.invoke('showSaveDialog', options),
log: (text)=> ipcRenderer.send('log', text),
reloadApp: ()=>{ipcRenderer.send('reloadApp');},
onFileDownload: (payload) => ipcRenderer.send('onFileDownload', payload),
});
+1
View File
@@ -1550,6 +1550,7 @@ __metadata:
axios: ^1.8.1
electron: 34.3.0
electron-context-menu: ^4.0.5
electron-dl: ^4.0.0
electron-store: ^10.0.0
eslint: ^9.21.0
languageName: unknown
+2 -17
View File
@@ -40,7 +40,7 @@ import Replication from './Replication';
import { getExpandCell } from '../../../static/js/components/PgReactTableStyled';
import CodeMirror from '../../../static/js/components/ReactCodeMirror';
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
import { getBrowser } from '../../../static/js/utils';
import { downloadFile } from '../../../static/js/utils';
import RefreshButton from './components/RefreshButtons';
function parseData(data) {
@@ -453,22 +453,7 @@ function Dashboard({
let fileName = 'data-' + new Date().getTime() + extension;
try {
let respBlob = new Blob([respData], {type : 'text/'+type}),
urlCreator = window.URL || window.webkitURL,
download_url = urlCreator.createObjectURL(respBlob),
link = document.createElement('a');
document.body.appendChild(link);
if (getBrowser() == 'IE' && window.navigator.msSaveBlob) {
// IE10: (has Blob, but not a[download] or URL)
window.navigator.msSaveBlob(respBlob, fileName);
} else {
link.setAttribute('href', download_url);
link.setAttribute('download', fileName);
link.click();
}
document.body.removeChild(link);
downloadFile(respData, fileName, `text/${type}`);
} catch {
setSsMsg(gettext('Failed to download the logs.'));
}
+28 -1
View File
@@ -15,7 +15,8 @@ from flask_babel import gettext
from pgadmin.user_login_check import pga_login_required
from pathlib import Path
from pgadmin.utils import PgAdminModule, get_binary_path_versions
from pgadmin.utils.constants import PREF_LABEL_USER_INTERFACE
from pgadmin.utils.constants import PREF_LABEL_USER_INTERFACE, \
PREF_LABEL_FILE_DOWNLOADS
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.session import cleanup_session_files
from pgadmin.misc.themes import get_all_themes
@@ -120,6 +121,32 @@ class MiscModule(PgAdminModule):
)
)
if not config.SERVER_MODE:
self.preference.register(
'file_downloads', 'automatically_open_downloaded_file',
gettext("Automatically open downloaded file?"),
'boolean', False,
category_label=PREF_LABEL_FILE_DOWNLOADS,
help_str=gettext(
'''This setting is applicable and visible only in
desktop mode. When set to True, the downloaded file
will automatically open in the system's default
application associated with that file type.'''
)
)
self.preference.register(
'file_downloads', 'prompt_for_download_location',
gettext("Prompt for the download location?"),
'boolean', True,
category_label=PREF_LABEL_FILE_DOWNLOADS,
help_str=gettext(
'This setting is applicable and visible only '
'in desktop mode. When set to True, a prompt '
'will appear after clicking the download button, '
'allowing you to choose the download location'
)
)
def get_exposed_url_endpoints(self):
"""
Returns:
@@ -7,6 +7,7 @@
//
//////////////////////////////////////////////////////////////
import getApiInstance from '../api_instance';
import { downloadFile } from '../utils';
function convertImageURLtoDataURI(api, image) {
return new Promise(function(resolve, reject) {
@@ -42,13 +43,6 @@ export function downloadSvg(svg, svgName) {
}
Promise.all(image_promises).then(function() {
let blob = new Blob([svgElement.outerHTML], {type: 'image/svg+xml'});
let svgURL = (window.URL || window.webkitURL).createObjectURL(blob);
let newElement = document.createElement('a');
newElement.href = svgURL;
newElement.setAttribute('download', svgName);
document.body.appendChild(newElement);
newElement.click();
document.body.removeChild(newElement);
downloadFile(svgElement.outerHTML, svgName, 'image/svg+xml');
});
}
+20 -5
View File
@@ -369,14 +369,16 @@ export function checkTrojanSource(content, isPasteEvent) {
}
}
export function downloadBlob(blob, fileName) {
if (getBrowser() == 'IE' && window.navigator.msSaveBlob) {
export async function downloadBlob(blob, fileName) {
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
const urlCreator = window.URL || window.webkitURL;
const downloadUrl = urlCreator.createObjectURL(blob);
if (getBrowser().name == 'IE' && window.navigator.msSaveBlob) {
// IE10+ : (has Blob, but not a[download] or URL)
window.navigator.msSaveBlob(blob, fileName);
} else if (getBrowser().name == 'Electron') {
await window.electronUI.onFileDownload({downloadUrl, fileName, automatically_open_downloaded_file, prompt_for_download_location});
} else {
const urlCreator = window.URL || window.webkitURL;
const downloadUrl = urlCreator.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', downloadUrl);
link.setAttribute('download', fileName);
@@ -388,6 +390,19 @@ export function downloadBlob(blob, fileName) {
}
}
export async function downloadUrlData(downloadUrl, fileName) {
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
if (getBrowser().name == 'Electron') {
window.electronUI.onFileDownload({downloadUrl, fileName, automatically_open_downloaded_file, prompt_for_download_location});
} else {
let link = document.createElement('a');
link.setAttribute('href', downloadUrl);
link.setAttribute('download', fileName);
link.click();
link.remove();
}
}
export function downloadFile(textData, fileName, fileType) {
const respBlob = new Blob([textData], {type : fileType});
downloadBlob(respBlob, fileName);
@@ -37,6 +37,7 @@ import pgAdmin from 'sources/pgadmin';
import { styled } from '@mui/material/styles';
import BeforeUnload from './BeforeUnload';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import { downloadUrlData } from '../../../../../../static/js/utils';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
@@ -760,11 +761,7 @@ export default class ERDTool extends React.Component {
}
toPng(this.canvasEle, {width, height})
.then((dataUrl)=>{
let link = document.createElement('a');
link.setAttribute('href', dataUrl);
link.setAttribute('download', this.getCurrentProjectName() + '.png');
link.click();
link.remove();
downloadUrlData(dataUrl, `${this.getCurrentProjectName()}.png`);
}).catch((err)=>{
console.error(err);
let msg = gettext('Unknown error. Check console logs');
@@ -26,7 +26,7 @@ import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE,
LightenDarkenColor} from 'sources/chartjs';
import { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent';
import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants';
import { getChartColor } from '../../../../../../static/js/utils';
import { downloadUrlData, getChartColor } from '../../../../../../static/js/utils';
const StyledBox = styled(Box)(({theme}) => ({
width: '100%',
@@ -377,11 +377,10 @@ export function GraphVisualiser({initColumns}) {
};
// Download button callback
const onDownloadGraph = ()=> {
let a = document.createElement('a');
a.href = chartObjRef.current.toBase64Image();
a.download = 'graph_visualiser-' + new Date().getTime() + '.png';
a.click();
const onDownloadGraph = async ()=> {
let downloadUrl = chartObjRef.current.toBase64Image(),
fileName = 'graph_visualiser-' + new Date().getTime() + '.png';
downloadUrlData(downloadUrl, fileName);
};
// This plugin is used to set the background color of the canvas. Very useful
@@ -22,7 +22,7 @@ import { LayoutDockerContext } from '../../../../../../static/js/helpers/Layout'
import { GeometryViewer } from './GeometryViewer';
import Explain from '../../../../../../static/js/Explain';
import { QuerySources } from './QueryHistory';
import { getBrowser } from '../../../../../../static/js/utils';
import { downloadFile } from '../../../../../../static/js/utils';
import CopyData from '../QueryToolDataGrid/CopyData';
import moment from 'moment';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
@@ -478,22 +478,7 @@ export class ResultSetUtils {
}
} else {
this.hasQueryCommitted = false;
let respBlob = new Blob([respData], {type : 'text/csv'}),
urlCreator = window.URL || window.webkitURL,
download_url = urlCreator.createObjectURL(respBlob),
link = document.createElement('a');
document.body.appendChild(link);
if (getBrowser() == 'IE' && window.navigator.msSaveBlob) {
// IE10+ : (has Blob, but not a[download] or URL)
window.navigator.msSaveBlob(respBlob, fileName);
} else {
link.setAttribute('href', download_url);
link.setAttribute('download', fileName);
link.click();
}
document.body.removeChild(link);
downloadFile(respData, fileName, 'text/csv');
}
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS_END);
} catch (error) {
+1
View File
@@ -29,6 +29,7 @@ PREF_LABEL_TABS_SETTINGS = gettext('Tab settings')
PREF_LABEL_REFRESH_RATES = gettext('Refresh rates')
PREF_LABEL_GRAPH_VISUALISER = gettext('Graph Visualiser')
PREF_LABEL_USER_INTERFACE = gettext('User Interface')
PREF_LABEL_FILE_DOWNLOADS = gettext('File Downloads')
PGADMIN_STRING_SEPARATOR = '_$PGADMIN$_'
PGADMIN_NODE = 'pgadmin.node.%s'