1
0
mirror of synced 2026-10-23 07:08:55 +03:00

Fix the server context menu showing database options after the tree state is restored. #7612 (#10580)

This commit is contained in:
Dave Page
2026-10-15 10:38:24 +01:00
parent 610cad0e0e
commit 472965ec40
3 changed files with 163 additions and 7 deletions

View File

@@ -474,10 +474,16 @@ define('pgadmin.node.database', [
pgBrowser.Events.trigger(
'pgadmin:database:connected', _item, _data
);
/* Call enable/disable menu function after database is connected.
To make sure all the menus for database is in the right state */
pgBrowser.enable_disable_menus(_item);
pgBrowser.Nodes['database'].callbacks.selected(_item, _data);
/* Refresh the menus after the database is connected, so that
they are in the right state. The menus always belong to the
selected node, which may not be this database: it can be
connected by expanding it, e.g. whilst the previous tree state
is restored, so only treat it as selected if it really is. */
if (_tree.selected() === _item) {
pgBrowser.Nodes['database'].callbacks.selected(_item, _data);
} else {
pgBrowser.enable_disable_menus();
}
if (!_connected) {
setTimeout(function() {

View File

@@ -698,9 +698,11 @@ define('pgadmin.node.server', [
// Load dashboard
pgBrowser.Events.trigger('pgadmin-browser:tree:selected', _item, _data, node);
/* Call enable/disable menu function after database is connected.
To make sure all the menus for database is in the right state */
pgBrowser.enable_disable_menus(_item);
/* Refresh the menus after the server is connected, so that they
are in the right state. They always belong to the selected node,
which may not be this server if it was connected by expanding
it, so do not pass the server's item here. */
pgBrowser.enable_disable_menus();
/* Below code is added to show the error message if
connection is successful, but there is an error to

View File

@@ -0,0 +1,148 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const mockPost = jest.fn();
jest.mock('../../../pgadmin/static/js/api_instance', () => ({
__esModule: true,
default: () => ({
post: mockPost, get: jest.fn(() => Promise.resolve({data: {}})),
delete: jest.fn(),
}),
parseApiError: (e) => e,
}));
jest.mock('../../../pgadmin/static/js/Dialogs/index', () => ({
showServerPassword: jest.fn(),
}));
const pgBrowser = {
Nodes: {},
Node: {
extend: (props) => ({...props, generate_url: () => '/connect'}),
callbacks: {selected: jest.fn()},
},
Collection: {extend: (props) => props},
Events: {trigger: jest.fn(), on: jest.fn()},
enable_disable_menus: jest.fn(),
notifier: {success: jest.fn(), info: jest.fn(), warning: jest.fn()},
stdW: {md: 650},
};
const pgAdmin = {Browser: pgBrowser};
// The node modules register themselves through an AMD style define(); load
// them with a stub that hands each factory our fake browser.
global.define = (_name, _deps, factory) => {
factory((s) => s, () => '', pgAdmin, pgBrowser, {id: 1}, {});
};
require('../../../pgadmin/browser/server_groups/servers/databases/static/js/database');
require('../../../pgadmin/browser/server_groups/servers/static/js/server');
delete global.define;
// Let the mocked API calls resolve and the success handlers run.
const flushPromises = async () => {
for (let i = 0; i < 5; i++) {
await Promise.resolve();
}
};
describe('database node connection', () => {
let tree, serverItem, dbItem, dbData;
beforeEach(() => {
jest.useFakeTimers();
serverItem = {id: 'server'};
dbItem = {id: 'db'};
dbData = {_type: 'database', connected: false, allowConn: true, icon: 'x'};
tree = {
selected: jest.fn(),
addIcon: jest.fn(),
removeIcon: jest.fn(),
select: jest.fn(),
open: jest.fn(),
};
pgBrowser.tree = tree;
pgBrowser.enable_disable_menus.mockClear();
pgBrowser.Node.callbacks.selected.mockClear();
mockPost.mockReset();
mockPost.mockResolvedValue({data: {
success: 1, info: 'Connected', data: {connected: true, icon: 'y'},
}});
});
afterEach(() => {
jest.useRealTimers();
});
const connectByExpanding = async () => {
const node = pgBrowser.Nodes['database'];
expect(node.callbacks.beforeopen.call(node, dbItem, dbData)).toBe(false);
await flushPromises();
};
it('keeps the menus on the selected server when a database is connected by expanding it', async () => {
tree.selected.mockReturnValue(serverItem);
await connectByExpanding();
expect(dbData.connected).toBe(true);
expect(pgBrowser.enable_disable_menus).toHaveBeenCalledWith();
expect(pgBrowser.enable_disable_menus).not.toHaveBeenCalledWith(dbItem);
expect(pgBrowser.Node.callbacks.selected).not.toHaveBeenCalled();
});
it('updates the menus for the database when it is the selected node', async () => {
tree.selected.mockReturnValue(dbItem);
await connectByExpanding();
expect(dbData.connected).toBe(true);
expect(pgBrowser.Node.callbacks.selected).toHaveBeenCalledWith(dbItem, dbData);
});
});
describe('server node connection', () => {
let tree, otherItem, serverItem, serverData;
beforeEach(() => {
jest.useFakeTimers();
otherItem = {id: 'other'};
serverItem = {id: 'server'};
serverData = {_type: 'server', _id: 1, connected: false, icon: 'x'};
tree = {
selected: jest.fn(() => otherItem),
addIcon: jest.fn(),
removeIcon: jest.fn(),
setLeaf: jest.fn(),
setInode: jest.fn(),
select: jest.fn(),
open: jest.fn(),
};
pgBrowser.tree = tree;
pgBrowser.enable_disable_menus.mockClear();
mockPost.mockReset();
mockPost.mockResolvedValue({data: {
success: 1, info: 'Connected',
data: {connected: true, icon: 'y', version: 180000},
}});
});
afterEach(() => {
jest.useRealTimers();
});
it('keeps the menus on the selected node when a server is connected by expanding it', async () => {
const node = pgBrowser.Nodes['server'];
expect(node.callbacks.beforeopen.call(node, serverItem, serverData)).toBe(false);
await flushPromises();
expect(serverData.connected).toBe(true);
expect(pgBrowser.enable_disable_menus).toHaveBeenCalledWith();
expect(pgBrowser.enable_disable_menus).not.toHaveBeenCalledWith(serverItem);
});
});