diff --git a/docs/en_US/images/preferences_browser_breadcrumbs.png b/docs/en_US/images/preferences_browser_breadcrumbs.png index 196b75a29..a0b4eea86 100644 Binary files a/docs/en_US/images/preferences_browser_breadcrumbs.png and b/docs/en_US/images/preferences_browser_breadcrumbs.png differ diff --git a/docs/en_US/images/preferences_browser_display.png b/docs/en_US/images/preferences_browser_display.png index 69a1d9421..b4ebcb74f 100644 Binary files a/docs/en_US/images/preferences_browser_display.png and b/docs/en_US/images/preferences_browser_display.png differ diff --git a/docs/en_US/images/preferences_browser_keyboard_shortcuts.png b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png index ac2885cd5..586c2399e 100644 Binary files a/docs/en_US/images/preferences_browser_keyboard_shortcuts.png and b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_browser_nodes.png b/docs/en_US/images/preferences_browser_nodes.png index 984b31c0a..4e07a3e84 100644 Binary files a/docs/en_US/images/preferences_browser_nodes.png and b/docs/en_US/images/preferences_browser_nodes.png differ diff --git a/docs/en_US/images/preferences_browser_processes.png b/docs/en_US/images/preferences_browser_processes.png index 4fb388440..b7815f26c 100644 Binary files a/docs/en_US/images/preferences_browser_processes.png and b/docs/en_US/images/preferences_browser_processes.png differ diff --git a/docs/en_US/images/preferences_browser_properties.png b/docs/en_US/images/preferences_browser_properties.png index 0c16cb577..235db7edc 100644 Binary files a/docs/en_US/images/preferences_browser_properties.png and b/docs/en_US/images/preferences_browser_properties.png differ diff --git a/docs/en_US/images/preferences_browser_tab_settings.png b/docs/en_US/images/preferences_browser_tab_settings.png index b15d73e78..48005a056 100644 Binary files a/docs/en_US/images/preferences_browser_tab_settings.png and b/docs/en_US/images/preferences_browser_tab_settings.png differ diff --git a/docs/en_US/images/preferences_dashboard_display.png b/docs/en_US/images/preferences_dashboard_display.png index 39f31c794..6098e1735 100644 Binary files a/docs/en_US/images/preferences_dashboard_display.png and b/docs/en_US/images/preferences_dashboard_display.png differ diff --git a/docs/en_US/images/preferences_dashboard_graphs.png b/docs/en_US/images/preferences_dashboard_graphs.png index 4fbf34fb7..d19dbb08b 100644 Binary files a/docs/en_US/images/preferences_dashboard_graphs.png and b/docs/en_US/images/preferences_dashboard_graphs.png differ diff --git a/docs/en_US/images/preferences_dashboard_refresh.png b/docs/en_US/images/preferences_dashboard_refresh.png index 5a2e0b7d9..586e35b5c 100644 Binary files a/docs/en_US/images/preferences_dashboard_refresh.png and b/docs/en_US/images/preferences_dashboard_refresh.png differ diff --git a/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png b/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png index 10ec903df..403fcaf5d 100644 Binary files a/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png and b/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_erd_keyboard_shortcuts.png b/docs/en_US/images/preferences_erd_keyboard_shortcuts.png index 3fda01a16..f1b7e0a31 100644 Binary files a/docs/en_US/images/preferences_erd_keyboard_shortcuts.png and b/docs/en_US/images/preferences_erd_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_erd_options.png b/docs/en_US/images/preferences_erd_options.png index 6f8b6878b..f729de9ce 100644 Binary files a/docs/en_US/images/preferences_erd_options.png and b/docs/en_US/images/preferences_erd_options.png differ diff --git a/docs/en_US/images/preferences_graph_visualiser.png b/docs/en_US/images/preferences_graph_visualiser.png index 92436ef58..e6cecb4f3 100644 Binary files a/docs/en_US/images/preferences_graph_visualiser.png and b/docs/en_US/images/preferences_graph_visualiser.png differ diff --git a/docs/en_US/images/preferences_header.png b/docs/en_US/images/preferences_header.png new file mode 100644 index 000000000..5202cf57a Binary files /dev/null and b/docs/en_US/images/preferences_header.png differ diff --git a/docs/en_US/images/preferences_misc_file_downloads.png b/docs/en_US/images/preferences_misc_file_downloads.png index fe8f7f981..cf0f3c560 100644 Binary files a/docs/en_US/images/preferences_misc_file_downloads.png and b/docs/en_US/images/preferences_misc_file_downloads.png differ diff --git a/docs/en_US/images/preferences_misc_user_interface.png b/docs/en_US/images/preferences_misc_user_interface.png index 841f9d918..9a884dbd8 100644 Binary files a/docs/en_US/images/preferences_misc_user_interface.png and b/docs/en_US/images/preferences_misc_user_interface.png differ diff --git a/docs/en_US/images/preferences_paths_binary.png b/docs/en_US/images/preferences_paths_binary.png index 1d07bf094..43e266d21 100644 Binary files a/docs/en_US/images/preferences_paths_binary.png and b/docs/en_US/images/preferences_paths_binary.png differ diff --git a/docs/en_US/images/preferences_paths_help.png b/docs/en_US/images/preferences_paths_help.png index bec54f9ac..ee21b38cf 100644 Binary files a/docs/en_US/images/preferences_paths_help.png and b/docs/en_US/images/preferences_paths_help.png differ diff --git a/docs/en_US/images/preferences_schema_diff.png b/docs/en_US/images/preferences_schema_diff.png index 336858494..ecdcec183 100644 Binary files a/docs/en_US/images/preferences_schema_diff.png and b/docs/en_US/images/preferences_schema_diff.png differ diff --git a/docs/en_US/images/preferences_sql_auto_completion.png b/docs/en_US/images/preferences_sql_auto_completion.png index e5bb3869e..a666afff2 100644 Binary files a/docs/en_US/images/preferences_sql_auto_completion.png and b/docs/en_US/images/preferences_sql_auto_completion.png differ diff --git a/docs/en_US/images/preferences_sql_csv_output.png b/docs/en_US/images/preferences_sql_csv_output.png index 1cd645daf..e779467cf 100644 Binary files a/docs/en_US/images/preferences_sql_csv_output.png and b/docs/en_US/images/preferences_sql_csv_output.png differ diff --git a/docs/en_US/images/preferences_sql_display.png b/docs/en_US/images/preferences_sql_display.png index 574e82184..caa2aa529 100644 Binary files a/docs/en_US/images/preferences_sql_display.png and b/docs/en_US/images/preferences_sql_display.png differ diff --git a/docs/en_US/images/preferences_sql_editor.png b/docs/en_US/images/preferences_sql_editor.png index 75e2aefe9..e417657e0 100644 Binary files a/docs/en_US/images/preferences_sql_editor.png and b/docs/en_US/images/preferences_sql_editor.png differ diff --git a/docs/en_US/images/preferences_sql_explain.png b/docs/en_US/images/preferences_sql_explain.png index 3709ca659..c84f3d395 100644 Binary files a/docs/en_US/images/preferences_sql_explain.png and b/docs/en_US/images/preferences_sql_explain.png differ diff --git a/docs/en_US/images/preferences_sql_formatting.png b/docs/en_US/images/preferences_sql_formatting.png index d121993b6..79a37eb44 100644 Binary files a/docs/en_US/images/preferences_sql_formatting.png and b/docs/en_US/images/preferences_sql_formatting.png differ diff --git a/docs/en_US/images/preferences_sql_keyboard_shortcuts.png b/docs/en_US/images/preferences_sql_keyboard_shortcuts.png index de61d4fa9..9f03a44f8 100644 Binary files a/docs/en_US/images/preferences_sql_keyboard_shortcuts.png and b/docs/en_US/images/preferences_sql_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_sql_options.png b/docs/en_US/images/preferences_sql_options.png index 452b9b631..3a7f222a9 100644 Binary files a/docs/en_US/images/preferences_sql_options.png and b/docs/en_US/images/preferences_sql_options.png differ diff --git a/docs/en_US/images/preferences_sql_results_grid.png b/docs/en_US/images/preferences_sql_results_grid.png index 4314d7d44..af538e6fe 100644 Binary files a/docs/en_US/images/preferences_sql_results_grid.png and b/docs/en_US/images/preferences_sql_results_grid.png differ diff --git a/docs/en_US/images/preferences_storage_options.png b/docs/en_US/images/preferences_storage_options.png index 75aa5ddf8..c4b1dd65b 100644 Binary files a/docs/en_US/images/preferences_storage_options.png and b/docs/en_US/images/preferences_storage_options.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 93138beab..b88ead5dc 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -2,24 +2,31 @@ .. _preferences: *************************** -`Preferences Dialog`:index: +`Preferences`:index: *************************** -Use options on the *Preferences* dialog to customize the behavior of the client. -To open the *Preferences* dialog, select *Preferences* from the *File* menu or +Use options in the *Preferences* tab to customize the behavior of the client. +To open the *Preferences* tab, select *Preferences* from the *File* menu or click on the *Settings* button at the bottom left corner in case of Workspace layout. -.. image:: images/preferences_menu.png - :alt: Preferences menu +Header +****** + +.. image:: images/preferences_header.png + :alt: Preferences browser display options :align: center -The left pane of the *Preferences* dialog displays a tree control; each node of +* Use the *Save* button to save any preference changes. +* Use the *Reset all preferences* button to restore all preferences to their default values. +* Use the *Help* button to open preferences help. +* Quickly search all preferences using the *Search* box. It finds matches in both labels and + help messages. + +The left pane of the *Preferences* tab displays a tree control; each node of the tree control provides access to options that are related to the node under which they are displayed. -* Click the *Reset all preferences* button to restore all preferences to their default values. - The Browser Node **************** @@ -27,7 +34,7 @@ Use preferences found in the *Browser* node of the tree control to personalize your workspace. .. image:: images/preferences_browser_display.png - :alt: Preferences dialog browser display options + :alt: Preferences browser display options :align: center Use the fields on the *Display* panel to specify general display preferences: @@ -64,7 +71,7 @@ Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the main window navigation: .. image:: images/preferences_browser_keyboard_shortcuts.png - :alt: Preferences dialog browser keyboard shortcuts section + :alt: Preferences browser keyboard shortcuts section :align: center * The panel displays a list of keyboard shortcuts available for the main window; @@ -75,7 +82,7 @@ Use the fields on the *Nodes* panel to select the object types that will be displayed in the *Browser* tree control: .. image:: images/preferences_browser_nodes.png - :alt: Preferences dialog browser nodes section + :alt: Preferences browser nodes section :align: center * The panel displays a list of database objects; slide the switch located next @@ -87,7 +94,7 @@ Use the fields on the *Object Breadcrumbs* panel to change object breadcrumbs related settings: .. image:: images/preferences_browser_breadcrumbs.png - :alt: Preferences dialog object breadcrumbs section + :alt: Preferences object breadcrumbs section :align: center * Use *Enable object breadcrumbs?* to enable or disable object breadcrumbs @@ -101,7 +108,7 @@ Use the fields on the *Processes* panel to change processes tab related settings: .. image:: images/preferences_browser_processes.png - :alt: Preferences dialog processes section + :alt: Preferences processes section :align: center * Change *Process details/logs retention days* to the number of days, @@ -110,7 +117,7 @@ related settings: Use fields on the *Properties* panel to specify browser properties: .. image:: images/preferences_browser_properties.png - :alt: Preferences dialog browser properties section + :alt: Preferences browser properties section :align: center * Include a value in the *Count rows if estimated less than* field to perform a @@ -124,7 +131,7 @@ Use fields on the *Properties* panel to specify browser properties: Use field on *Tab settings* panel to specify the tab related properties. .. image:: images/preferences_browser_tab_settings.png - :alt: Preferences dialog browser properties section + :alt: Preferences browser properties section :align: center * Use *Debugger tab title placeholder* field to customize the Debugger tab title. @@ -144,7 +151,7 @@ The Dashboards Node Expand the *Dashboards* node to specify your dashboard display preferences. .. image:: images/preferences_dashboard_display.png - :alt: Preferences dialog dashboard display options + :alt: Preferences dashboard display options :align: center * Set the warning and alert threshold value to highlight the long-running @@ -157,7 +164,7 @@ Expand the *Dashboards* node to specify your dashboard display preferences. dashboards. .. image:: images/preferences_dashboard_refresh.png - :alt: Preferences dialog dashboard refresh options + :alt: Preferences dashboard refresh options :align: center Use the fields on the *Refresh rates* panel to specify your refersh rates @@ -214,7 +221,7 @@ Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the debugger window navigation: .. image:: images/preferences_debugger_keyboard_shortcuts.png - :alt: Preferences dialog debugger keyboard shortcuts section + :alt: Preferences debugger keyboard shortcuts section :align: center The ERD Tool Node @@ -226,13 +233,13 @@ Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the ERD Tool window navigation: .. image:: images/preferences_erd_keyboard_shortcuts.png - :alt: Preferences dialog erd keyboard shortcuts section + :alt: Preferences erd keyboard shortcuts section :align: center Use the fields on the *Options* panel to manage ERD preferences. .. image:: images/preferences_erd_options.png - :alt: Preferences dialog erd options section + :alt: Preferences erd options section :align: center @@ -252,13 +259,13 @@ The Graphs Node Expand the *Graphs* node to specify your Graphs display preferences. -.. image:: images/preferences_dashboard_graphs.png - :alt: Preferences dialog dashboard graph options - :align: center - * Use the *Chart line width* field to specify the width of the lines on the line chart. +.. image:: images/preferences_dashboard_graphs.png + :alt: Preferences dashboard graph options + :align: center + * When the *Show graph data points?* switch is set to *True*, data points will be visible on graph lines. @@ -274,7 +281,7 @@ 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 + :alt: Preferences file downloads section :align: center Use the fields on the *File Downloads* panel to manage file downloads related preferences. @@ -292,7 +299,7 @@ Use the fields on the *File Downloads* panel to manage file downloads related pr 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 + :alt: Preferences user interface section :align: center * Use the *Language* drop-down listbox to select the display language for @@ -331,7 +338,7 @@ Expand the *Paths* node to specify the locations of supporting utility and help files. .. image:: images/preferences_paths_binary.png - :alt: Preferences dialog binary path section + :alt: Preferences binary path section :align: center Use the fields on the *Binary paths* panel to specify the path to the directory @@ -354,7 +361,7 @@ monitored databases: programs (pg_dump, pg_dumpall, pg_restore and psql) and there respective versions. .. image:: images/preferences_paths_help.png - :alt: Preferences dialog binary path help section + :alt: Preferences binary path help section :align: center Use the fields on the *Help* panel to specify the location of help files. @@ -372,7 +379,7 @@ Expand the *Query Tool* node to access panels that allow you to specify your preferences for the Query Editor tool. .. image:: images/preferences_sql_auto_completion.png - :alt: Preferences dialog sqleditor auto completion option + :alt: Preferences sqleditor auto completion option :align: center Use the fields on the *Auto Completion* panel to set the auto completion options. @@ -384,7 +391,7 @@ Use the fields on the *Auto Completion* panel to set the auto completion options shown in upper case. .. image:: images/preferences_sql_csv_output.png - :alt: Preferences dialog sqleditor csv output option + :alt: Preferences sqleditor csv output option :align: center Use the fields on the *CSV/TXT Output* panel to control the CSV/TXT output. @@ -399,7 +406,7 @@ Use the fields on the *CSV/TXT Output* panel to control the CSV/TXT output. specified string in the output file. Default is set to 'NULL'. .. image:: images/preferences_sql_display.png - :alt: Preferences dialog sqleditor display options + :alt: Preferences sqleditor display options :align: center Use the fields on the *Display* panel to specify your preferences for the Query @@ -415,7 +422,7 @@ Tool display. will show notifications on successful query execution. .. image:: images/preferences_sql_editor.png - :alt: Preferences dialog sqleditor editor settings + :alt: Preferences sqleditor editor settings :align: center Use the fields on the *Editor* panel to change settings of the query editor. @@ -451,7 +458,7 @@ Use the fields on the *Editor* panel to change settings of the query editor. highlight matched selected text. .. image:: images/preferences_sql_explain.png - :alt: Preferences dialog sqleditor explain options + :alt: Preferences sqleditor explain options :align: center Use the fields on the *Explain* panel to specify the level of detail included in @@ -480,7 +487,7 @@ a graphical EXPLAIN. will include extended information about the query execution plan. .. image:: images/preferences_graph_visualiser.png - :alt: Preferences dialog sqleditor graph visualiser section + :alt: Preferences sqleditor graph visualiser section :align: center Use the fields on the *Graph Visualiser* panel to specify the settings @@ -490,7 +497,7 @@ related to graphs. be plotted on a chart. .. image:: images/preferences_sql_options.png - :alt: Preferences dialog sqleditor options section + :alt: Preferences sqleditor options section :align: center Use the fields on the *Options* panel to manage editor preferences. @@ -536,7 +543,7 @@ Use the fields on the *Options* panel to manage editor preferences. will appear only if *Underline query at cursor?* is set to *False*. .. image:: images/preferences_sql_results_grid.png - :alt: Preferences dialog sql results grid section + :alt: Preferences sql results grid section :align: center Use the fields on the *Results grid* panel to specify your formatting @@ -564,14 +571,14 @@ preferences for copied data. rows with alternating background colors. .. image:: images/preferences_sql_keyboard_shortcuts.png - :alt: Preferences dialog sql keyboard shortcuts section + :alt: Preferences sql keyboard shortcuts section :align: center Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the Query Tool window navigation. .. image:: images/preferences_sql_formatting.png - :alt: Preferences dialog SQL Formatting section + :alt: Preferences SQL Formatting section :align: center Use the fields on the *SQL formatting* panel to specify your preferences for @@ -624,7 +631,7 @@ The Storage Node Expand the *Storage* node to specify your storage preferences. .. image:: images/preferences_storage_options.png - :alt: Preferences dialog storage section + :alt: Preferences storage section :align: center Use the fields on the *Options* panel to specify storage preferences. diff --git a/web/package.json b/web/package.json index 8f47b5705..05c6bd48d 100644 --- a/web/package.json +++ b/web/package.json @@ -81,6 +81,7 @@ "@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.0", "@mui/x-date-pickers": "^8.5.0", + "@nozbe/microfuzz": "^1.0.0", "@projectstorm/react-diagrams": "^7.0.4", "@simonwep/pickr": "^1.5.1", "@szhsin/react-menu": "^4.4.1", @@ -148,6 +149,7 @@ "sql-formatter": "^15.6.2", "uplot": "^1.6.32", "uplot-react": "^1.1.4", + "use-resize-observer": "^9.1.0", "valid-filename": "^4.0.0", "vanilla-jsoneditor": "^3.3.1", "wkx": "^0.5.0", diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js index 44fbac399..49773638c 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js @@ -402,7 +402,6 @@ export default class ForeignKeySchema extends BaseUISchema { },{ id: 'confdeltype', label: gettext('On delete'), type:'select', group: gettext('Action'), mode: ['edit','create'], - select2:{allowClear: false}, options: [ {label: 'NO ACTION', value: 'a'}, {label: 'RESTRICT', value: 'r'}, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.ui.js index ab820b34e..5a4c1cc24 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.ui.js @@ -81,7 +81,7 @@ export default class RuleSchema extends BaseUISchema { controlProps: { allowClear: false }, }, { - id: 'event', label: gettext('Event'), control: 'select2', + id: 'event', label: gettext('Event'), group: gettext('Definition'), type: 'select', controlProps: { allowClear: false }, options:[ diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py index 485faf4f2..e62f69334 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py @@ -402,7 +402,7 @@ class DataTypeReader: def parse_type_name(cls, type_name): """ Returns prase type name without length and precision - so that we can match the end result with types in the select2. + so that we can match the end result with types in the select. Args: self: self diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js index 8d12c475c..c69d88974 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js @@ -85,7 +85,6 @@ export default class ViewSchema extends BaseUISchema { type: 'select', group: gettext('Definition'), min_version: '90400', mode:['properties', 'create', 'edit'], controlProps: { - // Set select2 option width to 100% allowClear: false, }, disabled: obj.notInSchema, options:[{ diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js index 7521a7dfc..70cc0e8c1 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js @@ -361,7 +361,7 @@ export default class SubscriptionSchema extends BaseUISchema{ helpMessageMode: ['edit', 'create'], }, { - id: 'sync', label: gettext('Synchronous commit'), control: 'select2', deps:['event'], + id: 'sync', label: gettext('Synchronous commit'), deps:['event'], group: gettext('With'), type: 'select', helpMessage: gettext('The value of this parameter overrides the synchronous_commit setting. The default value is off.'), helpMessageMode: ['edit', 'create'], diff --git a/web/pgadmin/browser/static/js/constants.js b/web/pgadmin/browser/static/js/constants.js index 944752552..5cd74de96 100644 --- a/web/pgadmin/browser/static/js/constants.js +++ b/web/pgadmin/browser/static/js/constants.js @@ -29,6 +29,7 @@ export const BROWSER_PANELS = { DEPENDENCIES: 'id-dependencies', DEPENDENTS: 'id-dependents', PROCESSES: 'id-processes', + PREFERENCES: 'id-preferences', PROCESS_DETAILS: 'id-process-details', EDIT_PROPERTIES: 'id-edit-properties', UTILITY_DIALOG: 'id-utility', diff --git a/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js b/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js index 549921409..d891b6655 100644 --- a/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js +++ b/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js @@ -205,12 +205,9 @@ export default class BgProcessManager { } openProcessesPanel() { - let processPanel = this.pgBrowser.docker.default_workspace.find(BROWSER_PANELS.PROCESSES); - if(!processPanel) { - pgAdmin.Browser.docker.default_workspace.openTab(processesPanelData, BROWSER_PANELS.MAIN, 'middle', true); - } else { - this.pgBrowser.docker.default_workspace.focus(BROWSER_PANELS.PROCESSES); - } + let handler = this.pgBrowser.getDockerHandler?.(BROWSER_PANELS.PROCESSES, this.pgBrowser.docker.default_workspace); + handler.focus(); + handler.docker.openTab(processesPanelData, BROWSER_PANELS.MAIN, 'middle', true); } registerListener(event, callback) { diff --git a/web/pgadmin/preferences/static/js/components/LeftTree.jsx b/web/pgadmin/preferences/static/js/components/LeftTree.jsx new file mode 100644 index 000000000..dd6787b46 --- /dev/null +++ b/web/pgadmin/preferences/static/js/components/LeftTree.jsx @@ -0,0 +1,75 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useEffect, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Resizable } from 're-resizable'; +// Import helpers from new file + + +import PgTreeView from '../../../../static/js/PgTreeView'; + +export default function LeftTree({prefTreeData, selectedItem, setSelectedItem, filteredList}) { + const filteredTreeData = useMemo(() => { + const parentIds = filteredList.map((item) => item.parentId); + const filteredTreeData = prefTreeData.reduce((retVal, category) => { + const filteredChildren = category.children.filter((child) => parentIds.includes(child.id)); + if( filteredChildren.length > 0) { + retVal.push({ + ...category, + children: filteredChildren, + }); + } + return retVal; + }, []); + return filteredTreeData; + }, [prefTreeData, filteredList]); + + useEffect(() => { + // When the filtered list changes, we need to update the selected item + // to the first item in the filtered tree data, if available. + if (filteredTreeData.length > 0) { + setSelectedItem(filteredTreeData[0]?.children[0] ?? null); + } + }, [filteredList.length]); + + return ( + + { + setSelectedItem(item.data); + }} + // don't need virtualization for preferences tree + overscanCount={50} + /> + + ); +} + +LeftTree.propTypes = { + prefTreeData: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + children: PropTypes.array.isRequired, + })).isRequired, + selectedItem: PropTypes.object, + setSelectedItem: PropTypes.func.isRequired, + filteredList: PropTypes.array, +}; diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx index 9f05bcdbd..ffe864c4b 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -9,801 +9,410 @@ import gettext from 'sources/gettext'; import { styled } from '@mui/material/styles'; -import _ from 'lodash'; import url_for from 'sources/url_for'; -import React, { useEffect, useMemo } from 'react'; -import { FileType } from 'react-aspen'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Box } from '@mui/material'; import PropTypes from 'prop-types'; -import CloseIcon from '@mui/icons-material/CloseRounded'; -import HTMLReactParser from 'html-react-parser/lib/index'; -import SchemaView from '../../../../static/js/SchemaView'; import getApiInstance from '../../../../static/js/api_instance'; -import CloseSharpIcon from '@mui/icons-material/CloseSharp'; import HelpIcon from '@mui/icons-material/HelpRounded'; import SaveSharpIcon from '@mui/icons-material/SaveSharp'; -import SettingsBackupRestoreIcon from'@mui/icons-material/SettingsBackupRestore'; -import pgAdmin from 'sources/pgadmin'; -import { DefaultButton, PgIconButton, PrimaryButton } from '../../../../static/js/components/Buttons'; -import BaseUISchema from 'sources/SchemaView/base_schema.ui'; -import { getBinaryPathSchema } from '../../../../browser/server_groups/servers/static/js/binary_path.ui'; +import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; +import { PgButtonGroup, PgIconButton } from '../../../../static/js/components/Buttons'; import usePreferences from '../store'; -import { getBrowser } from '../../../../static/js/utils'; +import { usePgAdmin } from '../../../../static/js/PgAdminProvider'; +import { InputText } from '../../../../static/js/components/FormComponents'; +import { SearchRounded } from '@mui/icons-material'; +import PreferencesSchema from './preferences.ui'; +import { useFuzzySearchList } from '@nozbe/microfuzz/react'; +import Loader from 'sources/components/Loader'; -const StyledBox = styled(Box)(({theme}) => ({ - '& .PreferencesComponent-root': { +// Import helpers from new file +import { + reloadPgAdmin, + getNoteField, + prepareSubnodeData, + getCollectionValue, + showResetPrefModal +} from './PreferencesHelper'; +import { LAYOUT_EVENTS, LayoutDockerContext } from '../../../../static/js/helpers/Layout'; +import LeftTree from './LeftTree'; +import RightPreference from './RightPreference'; + +// --- Styled Components --- +const Root = styled(Box)(({ theme }) => ({ + height: '100%', + display: 'flex', + flexDirection: 'column', + background: theme.otherVars.emptySpaceBg, + overflow: 'hidden', + + '& .PreferencesComponent-header': { display: 'flex', - flexDirection: 'column', - flexGrow: 1, - height: '100%', - backgroundColor: theme.palette.background.default, - overflow: 'hidden', - '&$disabled': { - color: '#ddd', - }, - '& .PreferencesComponent-body': { - borderColor: theme.otherVars.borderColor, + alignItems: 'center', + background: theme.palette.background.default, + padding: theme.spacing(1), + ...theme.mixins.panelBorder.bottom, + + '& .PreferencesComponent-actionBtn': { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }, + + '& .PreferencesComponent-searchInput': { + maxWidth: '300px', + marginLeft: 'auto', + }, + }, + + '& .PreferencesComponent-body': { + flexGrow: 1, + minHeight: 0, + padding: theme.spacing(1), + + '& .PreferencesComponent-bodyWrap': { + ...theme.mixins.panelBorder.all, display: 'flex', - flexGrow: 1, height: '100%', - minHeight: 0, - overflow: 'hidden', + background: theme.palette.background.default, + '& .PreferencesComponent-treeContainer': { - flexBasis: '25%', - alignItems: 'flex-start', - paddingLeft: '5px', minHeight: 0, flexGrow: 1, - '& .PreferencesComponent-tree': { - height: '100%', - flexGrow: 1 - }, }, '& .PreferencesComponent-preferencesContainer': { - flexBasis: '75%', - padding: '5px', - borderColor: theme.otherVars.borderColor + '!important', + borderColor: `${theme.otherVars.borderColor} !important`, borderLeft: '1px solid', position: 'relative', height: '100%', - paddingTop: '5px', overflow: 'auto', - '& .PreferencesComponent-preferencesContainerBackground': { - backgroundColor: theme.palette.background.default, - } - }, - }, - '& .PreferencesComponent-footer': { - borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`, - padding: '0.5rem', - display: 'flex', - width: '100%', - background: theme.otherVars.headerBg, - '& .PreferencesComponent-actionBtn': { - alignItems: 'flex-start', - }, - '& .PreferencesComponent-buttonMargin': { - marginLeft: '0.5em' - }, - }, + width: '100%', + '& .PreferencesComponent-noSelection': { + padding: theme.spacing(1), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(0.5), + }, + + '& .PreferencesComponent-preferencesContainerBackground': { + backgroundColor: 'inherit', + }, + }, + }, }, - '& .Alert-footer': { - display: 'flex', - justifyContent: 'flex-end', + '& .PreferencesComponent-footer': { + borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`, padding: '0.5rem', - ...theme.mixins.panelBorder.top, - }, - '& .Alert-margin': { - marginLeft: '0.25rem', + display: 'flex', + width: '100%', + background: theme.otherVars.headerBg, + '& .PreferencesComponent-actionBtn': { + alignItems: 'flex-start', + }, + '& .PreferencesComponent-buttonMargin': { + marginLeft: '0.5em', + }, }, })); - -class PreferencesSchema extends BaseUISchema { - constructor(initValues = {}, schemaFields = []) { - super({ - ...initValues - }); - this.schemaFields = schemaFields; - this.category = ''; - } - - get idAttribute() { - return 'id'; - } - - categoryUpdated() { - this.state?.validate(this.sessData); - } - - get baseFields() { - return this.schemaFields; - } -} - -async function reloadPgAdmin() { - let {name: browser} = getBrowser(); - if(browser == 'Electron') { - await window.electronUI.log('test'); - await window.electronUI.reloadApp(); - } else { - location.reload(); - } -} - - -function RightPanel({ schema, refreshKey, ...props }) { - const schemaViewRef = React.useRef(null); - let initData = () => new Promise((resolve, reject) => { - try { - resolve(props.initValues); - } catch (error) { - reject(error instanceof Error ? error : Error(gettext('Something went wrong'))); - } - }); - useEffect(() => { - const timeID = setTimeout(() => { - const focusableElement = schemaViewRef.current?.querySelector( - 'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])' - ); - if (focusableElement) focusableElement.focus(); - }, 50); - return () => clearTimeout(timeID); - }, [refreshKey]); - - return ( -
- { - props.onDataChange(changedData); - }} - /> -
- ); -} - -RightPanel.propTypes = { - schema: PropTypes.object, - refreshKey: PropTypes.number, - initValues: PropTypes.object, - onDataChange: PropTypes.func +// Helper to check if a page refresh is required +function checkRefreshRequired(pref) { + // Other preferences might also require a refresh, add them here + return pref.name === 'user_language'; }; - -export default function PreferencesComponent({ ...props }) { - - const [refreshKey, setRefreshKey] = React.useState(0); - const [disableSave, setDisableSave] = React.useState(true); - const prefSchema = React.useRef(new PreferencesSchema({}, [])); - const prefChangedData = React.useRef({}); - const prefTreeInit = React.useRef(false); - const [prefTreeData, setPrefTreeData] = React.useState(null); - const [initValues, setInitValues] = React.useState({}); - const [loadTree, setLoadTree] = React.useState(0); +// --- Main PreferencesComponent --- +export default function PreferencesComponent({panelId}) { + const [disableSave, setDisableSave] = useState(true); + const prefSchema = useRef(new PreferencesSchema({}, [])); + const prefChangedData = useRef({}); + const [prefTreeData, setPrefTreeData] = useState([]); + const [initValues, setInitValues] = useState({}); const api = getApiInstance(); - const firstTreeElement = React.useRef(''); + const firstTreeElement = useRef(''); const preferencesStore = usePreferences(); + const pgAdmin = usePgAdmin(); + const [searchVal, setSearchVal] = useState(''); + const [selectedItem, setSelectedItem] = useState(null); + const [loaderText, setLoaderText] = useState(gettext('Loading preferences...')); + const layoutDocker = React.useContext(LayoutDockerContext); + const valuesVersionRef = useRef(); - useEffect(() => { - const pref_url = url_for('preferences.index'); - api({ - url: pref_url, - method: 'GET', - }).then((res) => { - let preferencesData = []; - let preferencesTreeData = []; - let preferencesValues = {}; - res.data.forEach(node => { - let id = crypto.getRandomValues(new Uint16Array(1)); - let tdata = { - 'id': id.toString(), - 'label': node.label, - '_label': node.label, - 'name': node.name, - 'icon': '', - 'inode': true, - 'type': 2, - '_type': node.label.toLowerCase(), - '_id': id, - '_pid': null, - 'childrenNodes': [], - 'expanded': true, - 'isExpanded': true, + const fetchPreferences = async () => { + setLoaderText(gettext('Loading preferences...')); + try { + const res = await api({ + url: url_for('preferences.index'), + method: 'GET', + }); + + const schemaFields = []; + const treeNodesData = []; + let values = {}; + + res.data.forEach((node) => { + const categoryNode = { + id: node.id.toString(), + name: node.label, + key: node.name, + children: [], }; - if(firstTreeElement.current.length == 0) { + if (firstTreeElement.current.length === 0) { firstTreeElement.current = node.label; } - node.children.forEach(subNode => { - let sid = crypto.getRandomValues(new Uint16Array(1)); - let nodeData = { - 'id': sid.toString(), - 'label': subNode.label, - '_label': subNode.label, - 'name': subNode.name, - 'icon': '', - 'inode': false, - '_type': subNode.label.toLowerCase(), - '_id': sid, - '_pid': node.id, - 'type': 1, - 'expanded': false, + node.children.forEach((subNode) => { + const nodeData = { + id: `${categoryNode.id}_${subNode.id}`, + name: subNode.label, + key: subNode.name, }; - addNote(node, subNode, nodeData, preferencesData); - setPreferences(node, subNode, nodeData, preferencesValues, preferencesData); - tdata['childrenNodes'].push(nodeData); + categoryNode.children.push(nodeData); + schemaFields.push(...getNoteField(node, subNode, nodeData)); + + const {fieldItems, fieldValues} = prepareSubnodeData(node, subNode, nodeData, preferencesStore); + schemaFields.push(...fieldItems); + values = {...values, ...fieldValues}; }); - - // set Preferences Tree data - preferencesTreeData.push(tdata); - + treeNodesData.push(categoryNode); }); - setPrefTreeData(preferencesTreeData); - setInitValues(preferencesValues); - // set Preferences schema - prefSchema.current = new PreferencesSchema( - preferencesValues, preferencesData, - ); - }).catch((err) => { - pgAdmin.Browser.notifier.alert(err); - }); - }, []); - function setPreferences( - node, subNode, nodeData, preferencesValues, preferencesData - ) { - let addBinaryPathNote = false; - subNode.preferences.forEach((element) => { - let note = ''; - let type = getControlMappedForType(element.type); - - if (type === 'file') { - note = gettext('Enter the directory in which the psql, pg_dump, pg_dumpall, and pg_restore utilities can be found for the corresponding database server version. The default path will be used for server versions that do not have a path specified.'); - element.type = 'collection'; - element.schema = getBinaryPathSchema(); - element.canAdd = false; - element.canDelete = false; - element.canEdit = false; - element.editable = false; - element.disabled = true; - preferencesValues[element.id] = JSON.parse(element.value); - if(addBinaryPathNote) { - addNote(node, subNode, nodeData, preferencesData, note); - } - addBinaryPathNote = true; - } - else if (type == 'select') { - setControlProps(element); - element.type = type; - preferencesValues[element.id] = element.value; - - setThemesOptions(element); - } - else if (type === 'keyboardShortcut') { - getKeyboardShortcuts(element, preferencesValues, node); - } else if (type === 'threshold') { - element.type = 'threshold'; - - let _val = element.value.split('|'); - preferencesValues[element.id] = { 'warning': _val[0], 'alert': _val[1] }; - } else if (subNode.label == gettext('Results grid') && node.label == gettext('Query Tool')) { - setResultsOptions(element, subNode, preferencesValues, type); - } else if (subNode.label == gettext('User Interface') && node.label == gettext('Miscellaneous')) { - setWorkspaceOptions(element, subNode, preferencesValues, type); - } else { - element.type = type; - preferencesValues[element.id] = element.value; - } - - delete element.value; - element.visible = false; - element.helpMessage = element?.help_str ? element.help_str : null; - preferencesData.push(element); - element.parentId = nodeData['id']; - }); - } - - function setResultsOptions(element, subNode, preferencesValues, type) { - if (element.name== 'column_data_max_width') { - let size_control_id = null; - subNode.preferences.forEach((_el) => { - if(_el.name == 'column_data_auto_resize') { - size_control_id = _el.id; - } - - }); - element.disabled = (state) => { - return state[size_control_id] != 'by_data'; - }; + valuesVersionRef.current = new Date().getTime(); + setPrefTreeData(treeNodesData); + setInitValues(values); + setSelectedItem(selectedItem || treeNodesData[0]?.children[0]); + prefSchema.current = new PreferencesSchema(values, schemaFields); + setLoaderText(null); + } catch (err) { + pgAdmin.Browser.notifier.alert(err.response?.data || err.message || gettext('Failed to load preferences.')); } - element.type = type; - preferencesValues[element.id] = element.value; - } - - function setWorkspaceOptions(element, subNode, preferencesValues, type) { - if (element.name== 'open_in_res_workspace') { - let layout_control_id = null; - subNode.preferences.forEach((_el) => { - if(_el.name == 'layout') { - layout_control_id = _el.id; - } - - }); - element.disabled = (state) => { - return state[layout_control_id] != 'workspace'; - }; - } - element.type = type; - preferencesValues[element.id] = element.value; - } - - function setThemesOptions(element) { - if (element.name == 'theme') { - element.type = 'theme'; - - element.options.forEach((opt) => { - if (opt.value == element.value) { - opt.selected = true; - } else { - opt.selected = false; - } - opt.preview_src = opt.preview_src && url_for('static', { filename: opt.preview_src }); - }); - } - } - function setControlProps(element) { - if (element.control_props !== undefined) { - element.controlProps = element.control_props; - } else { - element.controlProps = {}; - } - - } - - function getKeyboardShortcuts(element, preferencesValues, node) { - element.type = 'keyboardShortcut'; - element.canAdd = false; - element.canDelete = false; - element.canEdit = false; - element.editable = false; - if (preferencesStore.getPreferences(node.label.toLowerCase(), element.name)?.value) { - let temp = preferencesStore.getPreferences(node.label.toLowerCase(), element.name).value; - preferencesValues[element.id] = temp; - } else { - preferencesValues[element.id] = element.value; - } - } - function addNote(node, subNode, nodeData, preferencesData, note = '') { - // Check and add the note for the element. - if (subNode.label == gettext('Nodes') && node.label == gettext('Browser')) { - note = [gettext('This settings is to Show/Hide nodes in the object explorer.')].join(''); - } else { - note = [note].join(''); - } - - if (note && note.length > 0) { - //Add Note for Nodes - preferencesData.push( - { - id: _.uniqueId('note') + subNode.id, - type: 'note', - text: note, - 'parentId': nodeData['id'], - visible: false, - }, - ); - } - - } - - function selectChildNode(item, prefTreeInit) { - if (item.isExpanded && item._children && item._children.length > 0 && prefTreeInit.current && event.code !== 'ArrowUp') { - pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true); - } - } + }; + // Effect to fetch preferences data on component mount useEffect(() => { - let firstElement = null; - // Listen selected preferences tree node event and show the appropriate components in right panel. - pgAdmin.Browser.Events.on('preferences:tree:selected', (event, item) => { - if (item.type == FileType.File) { - prefSchema.current.schemaFields.forEach((field) => { - field.visible = field.parentId === item._metadata.data.id && - !field?.hidden ; + fetchPreferences(); + }, []); // Added dependencies - if(field.visible && _.isNull(firstElement)) { - firstElement = field; - } - - field.labelTooltip = - item._parent._metadata.data.name.toLowerCase() + ':' + - item._metadata.data.name + ':' + field.name; - }); - prefSchema.current.categoryUpdated(item._metadata.data.id); - setLoadTree(Date.now()); - setRefreshKey(Date.now()); - } - else { - selectChildNode(item, prefTreeInit); - } - }); - - // Listen open preferences tree node event to default select first child node on parent node selection. - pgAdmin.Browser.Events.on('preferences:tree:opened', (event, item) => { - pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true); - }); - - // Listen added preferences tree node event to expand the newly added node on tree load. - pgAdmin.Browser.Events.on('preferences:tree:added', addPrefTreeNode); - }, []); - - function addPrefTreeNode(event, item) { - if (item._parent._fileName == firstTreeElement.current && item._parent.isExpanded && !prefTreeInit.current) { - pgAdmin.Browser.ptree.tree.setActiveFile(item._parent._children[0], true); - } - else if (item.type == FileType.Directory) { - // Check the if newely added node is Directoy and call toggle to expand the node. - pgAdmin.Browser.ptree.tree.toggleDirectory(item); - } - } - - function getControlMappedForType(type) { - switch (type) { - case 'text': - return 'text'; - case 'input': - return 'text'; - case 'boolean': - return 'switch'; - case 'node': - return 'switch'; - case 'integer': - return 'numeric'; - case 'numeric': - return 'numeric'; - case 'date': - return 'datetimepicker'; - case 'datetime': - return 'datetimepicker'; - case 'options': - return 'select'; - case 'select': - return 'select'; - case 'select2': - return 'select'; - case 'multiline': - return 'multiline'; - case 'switch': - return 'switch'; - case 'keyboardshortcut': - return 'keyboardShortcut'; - case 'radioModern': - return 'toggle'; - case 'selectFile': - return 'file'; - case 'threshold': - return 'threshold'; - default: - if (console?.warn) { - // Warning for developer only. - console.warn( - 'Hmm.. We don\'t know how to render this type - \'\'' + type + '\' of control.' - ); - } - return 'input'; - } - } - - function getCollectionValue(_metadata, value, initVals) { - let val = value; - if (typeof (value) == 'object') { - if (_metadata[0].type == 'collection' && _metadata[0].schema) { - if ('binaryPath' in value.changed[0]) { - let pathData = []; - let pathVersions = []; - value.changed.forEach((chValue) => { - pathVersions.push(chValue.version); - }); - getPathData(initVals, pathData, _metadata, value, pathVersions); - val = JSON.stringify(pathData); - } else { - let key_val = { - 'char': value.changed[0]['key'], - 'key_code': value.changed[0]['code'], - }; - value.changed[0]['key'] = key_val; - val = value.changed[0]; - } - } else if ('warning' in value) { - val = value['warning'] + '|' + value['alert']; - } else if (value?.changed && value.changed.length > 0) { - val = JSON.stringify(value.changed); - } - } - return val; - } - - function getPathData(initVals, pathData, _metadata, value, pathVersions) { - initVals[_metadata[0].id].forEach((initVal) => { - if (pathVersions.includes(initVal.version)) { - pathData.push(value.changed[pathVersions.indexOf(initVal.version)]); - } - else { - pathData.push(initVal); - } - }); - } - - function savePreferences(data, initVal) { - let _data = []; - for (const [key, value] of Object.entries(data.current)) { - let _metadata = prefSchema.current.schemaFields.filter( - (el) => { return el.id == key; } - ); - if (_metadata.length > 0) { - let val = getCollectionValue(_metadata, value, initVal); - _data.push({ - 'category_id': _metadata[0]['cid'], - 'id': parseInt(key), - 'mid': _metadata[0]['mid'], - 'name': _metadata[0]['name'], - 'value': val, - }); - } - } - - if (_data.length > 0) { - // Check whether layout is changed from Workspace to Classic. - let layoutPref = _data.find(x => x.name === 'layout'); - // If layout is changed then raise the warning to close all the connections. - if (!_.isUndefined(layoutPref) && layoutPref.value == 'classic') { + useEffect(()=>{ + /* Bind the close event and check if user should be warned */ + const deregister = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, ()=>{ + if(Object.keys(prefChangedData.current).length > 0) { pgAdmin.Browser.notifier.confirm( - gettext('Layout changed'), - `${gettext('Switching from Workspace to Classic layout will disconnect all server connections and refresh the entire page.')} - ${gettext('To avoid losing unsaved data, click Cancel to manually review and close your connections.')} - ${gettext('Note that if you choose Cancel, any changes to your preferences will not be saved.')}

- ${gettext('Do you want to continue?')}`, - function () { - save(_data, data, true); - }, - function () { + gettext('Warning'), + gettext('Changes will be lost. Are you sure you want to close the preferences?'), + function() { + layoutDocker.close(panelId, true); return true; }, - gettext('Continue'), - gettext('Cancel') + null ); - } else { - save(_data, data); + return false; // Prevent closing + } + layoutDocker.close(panelId, true); + }); + return ()=>{ + deregister(); + }; + }, []); + + + const savePreferences = async () => { + const _data = []; + setLoaderText(gettext('Saving preferences...')); + for (const [key, value] of Object.entries(prefChangedData.current)) { + const _metadata = prefSchema.current.schemaFields.find((el) => el.id == key); // Find directly + if (_metadata) { + const val = getCollectionValue([_metadata], value, initValues); // Pass _metadata as array for consistency + _data.push({ + category_id: _metadata.cid, + id: parseInt(key), + mid: _metadata.mid, + name: _metadata.name, + value: val, + }); } } - } - - function checkRefreshRequired(pref, requires_refresh) { - if (pref.name == 'user_language') { - requires_refresh = true; + if (_data.length === 0) { + // No changes to save, just close modal + return; } - return requires_refresh; - } + const layoutPref = _data.find((x) => x.name === 'layout'); - function save(save_data, data, layout_changed=false) { - api({ - url: url_for('preferences.index'), - method: 'PUT', - data: save_data, - }).then(() => { - // If layout is changed then only refresh the object explorer. - if (layout_changed) { - api({ - url: url_for('workspace.layout_changed'), - method: 'DELETE', - data: save_data, - }).then(() => { - pgAdmin.Browser.tree.destroy().then( - () => { - pgAdmin.Browser.Events.trigger( - 'pgadmin-browser:tree:destroyed', undefined, undefined - ); - return true; + const saveData = async (shouldReloadOnLayoutChange = false) => { + try { + await api({ + url: url_for('preferences.index'), + method: 'PUT', + data: _data, + }); + + if (shouldReloadOnLayoutChange) { + await api({ + url: url_for('workspace.layout_changed'), + method: 'DELETE', // DELETE seems unusual for layout_changed, but maintaining original logic + data: _data, + }); + pgAdmin.Browser.tree.destroy().then(() => { + pgAdmin.Browser.Events.trigger('pgadmin-browser:tree:destroyed', undefined, undefined); + reloadPgAdmin(); // Reload after destroying tree + }); + } else { + const requiresTreeRefresh = _data.some((s) => + ['show_system_objects', 'show_empty_coll_nodes', 'hide_shared_server', 'show_user_defined_templates'].includes(s.name) || s.name.startsWith('show_node_') + ); + + let requiresFullPageRefresh = false; + for (const key of Object.keys(prefChangedData.current)) { + const pref = preferencesStore.getPreferenceForId(Number(key)); + if (pref && checkRefreshRequired(pref)) { + requiresFullPageRefresh = true; + break; } - ); - }); - } else { - let requiresTreeRefresh = save_data.some((s)=>{ - return ( - s.name=='show_system_objects' || s.name=='show_empty_coll_nodes' || - s.name.startsWith('show_node_') || s.name=='hide_shared_server' || - s.name=='show_user_defined_templates' - ); - }); - let requires_refresh = false; - for (const [key] of Object.entries(data.current)) { - let pref = preferencesStore.getPreferenceForId(Number(key)); - requires_refresh = checkRefreshRequired(pref, requires_refresh); - } + } - if (requiresTreeRefresh) { - pgAdmin.Browser.notifier.confirm( - gettext('Object explorer refresh required'), - gettext( - 'An object explorer refresh is required. Do you wish to refresh it now?' - ), - function () { - pgAdmin.Browser.tree.destroy().then( - () => { - pgAdmin.Browser.Events.trigger( - 'pgadmin-browser:tree:destroyed', undefined, undefined - ); - return true; - } - ); - }, - function () { - return true; - }, - gettext('Refresh'), - gettext('Later') - ); - } + if (requiresTreeRefresh) { + pgAdmin.Browser.notifier.confirm( + gettext('Object explorer refresh required'), + gettext('An object explorer refresh is required. Do you wish to refresh it now?'), + () => { + pgAdmin.Browser.tree.destroy().then(() => { + pgAdmin.Browser.Events.trigger('pgadmin-browser:tree:destroyed', undefined, undefined); + }); + return true; + }, + () => true, + gettext('Refresh'), + gettext('Later') + ); + } - if (requires_refresh) { - pgAdmin.Browser.notifier.confirm( - gettext('Refresh required'), - gettext('A page refresh is required. Do you wish to refresh the page now?'), - function () { - /* If user clicks Yes */ - reloadPgAdmin(); - return true; - }, - function () { props.closeModal();}, - gettext('Refresh'), - gettext('Later') - ); + if (requiresFullPageRefresh) { + pgAdmin.Browser.notifier.confirm( + gettext('Refresh required'), + gettext('A page refresh is required. Do you wish to refresh the page now?'), + () => { + reloadPgAdmin(); + return true; + }, + () => { }, // Close modal if user opts for "Later" + gettext('Refresh'), + gettext('Later') + ); + } } + preferencesStore.cache(); // Refresh preferences cache + fetchPreferences(); + } catch (err) { + pgAdmin.Browser.notifier.alert(err.response?.data || err.message || gettext('Failed to save preferences.')); } - // Refresh preferences cache - preferencesStore.cache(); - props.closeModal(); - }).catch((err) => { - pgAdmin.Browser.notifier.alert(err.response.data); - }); - } + }; - const onDialogHelp = () => { - window.open(url_for('help.static', { 'filename': 'preferences.html' }), 'pgadmin_help'); - }; - - const reset = () => { - const text = `${gettext('All preferences will be reset to their default values.')}

${gettext('Do you want to proceed?')}

-${gettext('Note:')}
`; - pgAdmin.Browser.notifier.showModal( - gettext('Reset all preferences'), - (closeModal)=>{ - const onClick = (reset) => { - resetPrefsToDefault(reset); - closeModal(); - }; - return( - - - {HTMLReactParser(text)} - - - } onClick={()=> closeModal()}>{'Cancel'} - } onClick={() => onClick(true)} >{gettext('Save & Reload')} - } onClick={()=>onClick(false)}>{gettext('Save & Reload Later')} - - - ); - }, - { isFullScreen: false, isResizeable: false, showFullScreen: false, isFullWidth: false, showTitle: true, id: 'id-reset-preferences'}, - ); - }; - - const resetPrefsToDefault = (refresh = false) => { - api({ - url: url_for('preferences.index'), - method: 'DELETE' - }).then(()=>{ - if (refresh){ - reloadPgAdmin(); - return true; - } - preferencesStore.cache(); - pgAdmin.Browser.tree.destroy().then( - () => { - pgAdmin.Browser.Events.trigger( - 'pgadmin-browser:tree:destroyed', undefined, undefined - ); - return true; - } + if (layoutPref && layoutPref.value === 'classic') { + pgAdmin.Browser.notifier.confirm( + gettext('Layout changed'), + `${gettext('Switching from Workspace to Classic layout will disconnect all server connections and refresh the entire page.')} + ${gettext('To avoid losing unsaved data, click Cancel to manually review and close your connections.')} + ${gettext('Note that if you choose Cancel, any changes to your preferences will not be saved.')}

+ ${gettext('Do you want to continue?')}`, + () => saveData(true), // User confirms, proceed with reload + () => false, // User cancels, do nothing + gettext('Continue'), + gettext('Cancel') ); - props.closeModal(); - }).catch((err) => { - pgAdmin.Browser.notifier.alert(err.response.data); + } else { + saveData(); + } + }; + + const resetAllPreferences = () => { + showResetPrefModal(api, pgAdmin, preferencesStore, ()=>{ + fetchPreferences(); }); }; + const filteredList = useFuzzySearchList({ + strategy: 'off', + queryText: searchVal, + getText: (item) => [item.label, item.helpMessage], + list: prefSchema.current.schemaFields, + mapResultItem: ({ item }) => item + }); + + const filteredItemIds = useMemo(()=>filteredList.map((item) => item.id), [filteredList]); + return ( - - - - - - { - useMemo( - () => (prefTreeData && props.renderTree(prefTreeData)), - [prefTreeData] - ) - } - - - - { - prefSchema.current && loadTree > 0 && - { - Object.keys(changedData).length > 0 ? - setDisableSave(false) : setDisableSave(true); - prefChangedData.current = changedData; - }} - > - } - - - - + + + + } title={gettext('Help for this dialog.')} + icon={} + aria-label="Save" + title={gettext('Save')} + onClick={savePreferences} + disabled={disableSave || Boolean(loaderText)} /> - - - }> - {gettext('Reset all preferences')} - - { props.closeModal();}} - startIcon={ - { props.closeModal();}} /> - }> - {gettext('Cancel')} - - } - disabled={disableSave} - onClick={() => { - savePreferences(prefChangedData, initValues); - }}> - {gettext('Save')} - - + + + } + aria-label="Reset all preferences" + title={gettext('Reset all preferences')} + /> + { + window.open(url_for('help.static', { filename: 'preferences.html' }), 'pgadmin_help'); + }} + icon={} title={gettext('Help')} + /> + + } + /> + + + +
+ + { + prefSchema.current && + { + setDisableSave(Object.keys(changedData).length === 0); + prefChangedData.current = changedData; + }} + /> + } +
-
+ ); } PreferencesComponent.propTypes = { - schema: PropTypes.array, - initValues: PropTypes.object, - closeModal: PropTypes.func, - renderTree: PropTypes.func + panelId: PropTypes.string.isRequired, }; diff --git a/web/pgadmin/preferences/static/js/components/PreferencesHelper.jsx b/web/pgadmin/preferences/static/js/components/PreferencesHelper.jsx new file mode 100644 index 000000000..5ca81532d --- /dev/null +++ b/web/pgadmin/preferences/static/js/components/PreferencesHelper.jsx @@ -0,0 +1,252 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import { styled } from '@mui/material/styles'; +import _ from 'lodash'; +import url_for from 'sources/url_for'; +import React from 'react'; +import { Box } from '@mui/material'; +import { DefaultButton, PrimaryButton } from '../../../../static/js/components/Buttons'; +import { getBinaryPathSchema } from './binary_path.ui'; +import { getBrowser } from '../../../../static/js/utils'; +import SaveSharpIcon from '@mui/icons-material/SaveSharp'; +import CloseIcon from '@mui/icons-material/CloseRounded'; +import HTMLReactParser from 'html-react-parser/lib/index'; + + +export async function reloadPgAdmin() { + const { name: browser } = getBrowser(); + if (browser === 'Electron') { + await window.electronUI.reloadApp(); + } else { + location.reload(); + } +} + +export function getNoteField(node, subNode, nodeData, note = '') { + // Check and add the note for the element. + if (subNode.label === gettext('Nodes') && node.label === gettext('Browser')) { + note = gettext('This settings is to Show/Hide nodes in the object explorer.'); + } + + if (note.length > 0) { + return [{ + id: _.uniqueId('note_') + subNode.id, // Better unique ID prefix + type: 'note', + text: note, + parentId: nodeData.id, + visible: false, + }]; + } + return []; +} + +export function prepareSubnodeData(node, subNode, nodeData, preferencesStore) { + let addBinaryPathNote = false; + let fieldItems = []; + let fieldValues = {}; + const typeMap = { + text: 'text', + input: 'text', + boolean: 'switch', + node: 'switch', + integer: 'numeric', + numeric: 'numeric', + date: 'datetimepicker', + datetime: 'datetimepicker', + options: 'select', + select: 'select', + multiline: 'multiline', + switch: 'switch', + keyboardshortcut: 'keyboardShortcut', + radioModern: 'toggle', + threshold: 'threshold', + }; + + subNode.preferences.forEach((element) => { + let type = typeMap[element.type] || element.type; + let note = ''; // Initialize note for each element + + // Ensure type is set after specific handling + element.type = type; + if (type === 'selectFile') { + // Binary Path specific handling + note = gettext('Enter the directory in which the psql, pg_dump, pg_dumpall, and pg_restore utilities can be found for the corresponding database server version. The default path will be used for server versions that do not have a path specified.'); + element.type = 'collection'; + element.schema = getBinaryPathSchema(); + element.canAdd = false; + element.canDelete = false; + element.canEdit = false; + element.editable = false; + element.disabled = true; // Binary paths are managed in a collection, not directly editable here + fieldValues[element.id] = JSON.parse(element.value); + if (!addBinaryPathNote) { // Add note only once for binary path section + fieldItems.push(...getNoteField(node, subNode, nodeData, note)); + addBinaryPathNote = true; + } + } else if (type === 'select') { + element.controlProps = element.control_props ?? {}; + fieldValues[element.id] = element.value; + + if (element.name === 'theme') { + element.type = 'theme'; + element.options.forEach((opt) => { + opt.selected = opt.value === element.value; + opt.preview_src = opt.preview_src && url_for('static', { filename: opt.preview_src }); + }); + } + } else if (type === 'keyboardShortcut') { + element.type = 'keyboardShortcut'; + element.canAdd = false; + element.canDelete = false; + element.canEdit = false; + element.editable = false; + + const storedValue = preferencesStore.getPreferences(node.label.toLowerCase(), element.name)?.value; + fieldValues[element.id] = storedValue || element.value; + } else if (type === 'threshold') { + element.type = 'threshold'; + const _val = element.value.split('|'); + fieldValues[element.id] = { warning: _val[0], alert: _val[1] }; + } else if (subNode.label === gettext('Results grid') && node.label === gettext('Query Tool')) { + if (element.name === 'column_data_max_width') { + const sizeControl = subNode.preferences.find((_el) => _el.name === 'column_data_auto_resize'); + if (sizeControl) { + element.disabled = (state) => state[sizeControl.id] !== 'by_data'; + } + } + element.type = type; + fieldValues[element.id] = element.value; + } else if (subNode.label === gettext('User Interface') && node.label === gettext('Miscellaneous')) { + if (element.name === 'open_in_res_workspace') { + const layoutControl = subNode.preferences.find((_el) => _el.name === 'layout'); + if (layoutControl) { + element.disabled = (state) => state[layoutControl.id] !== 'workspace'; + } + } + element.type = type; + fieldValues[element.id] = element.value; + } else { + fieldValues[element.id] = element.value; + } + + delete element.value; // Original value is moved to fieldValues + element.visible = false; + element.helpMessage = element?.help_str || null; + element.parentId = nodeData.id; + fieldItems.push(element); + }); + return { fieldItems, fieldValues }; +} + +export function getCollectionValue(_metadata, value, initVals) { + let val = value; + if (typeof value === 'object' && value !== null) { // Ensure value is an object and not null + const meta = _metadata[0]; // Assuming _metadata will always have at least one element relevant to the current field + + if (meta.type === 'collection' && meta.schema) { + if (value.changed?.[0] && 'binaryPath' in value.changed[0]) { + const pathData = []; + const pathVersions = value.changed.map(chValue => chValue.version); + + initVals[meta.id].forEach((initVal) => { + const changedIndex = pathVersions.indexOf(initVal.version); + if (changedIndex !== -1) { + pathData.push(value.changed[changedIndex]); + } else { + pathData.push(initVal); + } + }); + val = JSON.stringify(pathData); + } else if (value.changed?.[0]) { // Generic collection, likely keyboard shortcut + const changedEntry = value.changed[0]; + if ('key' in changedEntry && 'code' in changedEntry) { + changedEntry.key = { + 'char': changedEntry.key, // Original `key` is now `char` + 'key_code': changedEntry.code, // Original `code` is now `key_code` + }; + delete changedEntry.code; // Remove old code + } + val = changedEntry; // Changed to object + } + } else if ('warning' in value && 'alert' in value) { // Threshold type + val = `${value.warning}|${value.alert}`; + } else if (value.changed && value.changed.length > 0) { // Catch-all for other collections/arrays + val = JSON.stringify(value.changed); + } + } + return val; +} + +const StyledBox = styled(Box)(({ theme }) => ({ + '& .Alert-footer': { + display: 'flex', + justifyContent: 'flex-end', + padding: '0.5rem', + ...theme.mixins.panelBorder.top, + }, + '& .Alert-margin': { + marginLeft: '0.25rem', + }, +})); + + +export function showResetPrefModal(api, pgAdmin, preferencesStore, onReset) { + pgAdmin.Browser.notifier.showModal( + gettext('Reset all preferences'), + (modalClose) => { + const handleResetClick = async (reloadNow) => { + try { + await api({ + url: url_for('preferences.index'), + method: 'DELETE', + }); + preferencesStore.cache(); // Refresh preferences cache + onReset(); + if (reloadNow) { + reloadPgAdmin(); + } else { + pgAdmin.Browser.tree.destroy().then(() => { + pgAdmin.Browser.Events.trigger('pgadmin-browser:tree:destroyed', undefined, undefined); + modalClose(); // Close modal after tree destruction if no full reload + }); + } + } catch (err) { + pgAdmin.Browser.notifier.alert(err.response?.data || err.message || gettext('Failed to reset preferences.')); + modalClose(); + } + }; + + const text = `${gettext('All preferences will be reset to their default values.')}

${gettext('Do you want to proceed?')}

+ ${gettext('Note:')}
`; + + return ( + + + {HTMLReactParser(text)} + + + } onClick={modalClose}> + {gettext('Cancel')} + + } onClick={() => handleResetClick(true)}> + {gettext('Save & Reload')} + + } onClick={() => handleResetClick(false)}> + {gettext('Save & Reload Later')} + + + + ); + }, + { isFullScreen: false, isResizeable: false, showFullScreen: false, isFullWidth: false, showTitle: true, id: 'id-reset-preferences' } + ); +} diff --git a/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx b/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx deleted file mode 100644 index be6e65a6f..000000000 --- a/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx +++ /dev/null @@ -1,85 +0,0 @@ -// ///////////////////////////////////////////////////////////// -// // -// // pgAdmin 4 - PostgreSQL Tools -// // -// // Copyright (C) 2013 - 2025, The pgAdmin Development Team -// // This software is released under the PostgreSQL Licence -// // -// ////////////////////////////////////////////////////////////// - -import gettext from 'sources/gettext'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { Directory} from 'react-aspen'; -import { Tree } from '../../../../static/js/tree/tree'; -import { ManagePreferenceTreeNodes } from '../../../../static/js/tree/preference_nodes'; -import pgAdmin from 'sources/pgadmin'; -import { FileTreeX, TreeModelX } from '../../../../static/js/components/PgTree'; - - -export default function PreferencesTree({ pgBrowser, data }) { - const pTreeModelX = React.useRef(); - const onReadyRef = React.useRef(); - const [loaded, setLoaded] = React.useState(false); - - const MOUNT_POINT = '/preferences'; - - React.useEffect(() => { - setLoaded(false); - - // Setup host - let ptree = new ManagePreferenceTreeNodes(data); - // Init Tree with the Tree Parent node '/browser' - ptree.init(MOUNT_POINT); - - const host = { - pathStyle: 'unix', - getItems: (path) => { - return ptree.readNode(path); - - }, - sortComparator: (a, b) => { - // No nee to sort Query tool options. - if (a._parent && a._parent._fileName == gettext('Query Tool')) return 0; - // Sort alphabetically - if (a.constructor === b.constructor) { - return pgAdmin.natural_sort(a.fileName, b.fileName); - } - let retval = 0; - if (a.constructor === Directory) { - retval = -1; - } else if (b.constructor === Directory) { - retval = 1; - } - return retval; - }, - }; - - pTreeModelX.current = new TreeModelX(host, MOUNT_POINT); - onReadyRef.current = function onReady(handler) { - // Initialize preferences Tree - pgBrowser.ptree = new Tree(handler, ptree, pgBrowser, 'preferences'); - // Expand directoy on loading. - pTreeModelX.current.root._children.forEach((_d)=> { - _d.root.expandDirectory(_d); - }); - - return true; - }; - - pTreeModelX.current.root.ensureLoaded().then(() => { - setLoaded(true); - }); - }, [data]); - - if (!loaded || _.isUndefined(pTreeModelX.current) || _.isUndefined(onReadyRef.current)) { - return (gettext('Loading...')); - } - return (); -} - -PreferencesTree.propTypes = { - pgBrowser: PropTypes.any, - data: PropTypes.array, - ptree: PropTypes.any, -}; diff --git a/web/pgadmin/preferences/static/js/components/RightPreference.jsx b/web/pgadmin/preferences/static/js/components/RightPreference.jsx new file mode 100644 index 000000000..19f37ccf7 --- /dev/null +++ b/web/pgadmin/preferences/static/js/components/RightPreference.jsx @@ -0,0 +1,87 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import React, { useEffect, useRef, useCallback } from 'react'; +import { Box, Link } from '@mui/material'; +import PropTypes from 'prop-types'; +import SchemaView from '../../../../static/js/SchemaView'; +// Import helpers from new file + + + +export default function RightPreference({ schema, filteredItemIds, selectedItem, setSelectedItem, initValues, onDataChange }) { + const schemaViewRef = useRef(null); + + const getInitData = useCallback(() => { + return new Promise((resolve, reject) => { + try { + resolve(initValues); + } catch (error) { + reject(error instanceof Error ? error : Error(gettext('Something went wrong'))); + } + }); + }, [initValues]); + + const updateVisibleFields = () => { + if(!selectedItem) return; + + schema.schemaFields.forEach((field) => { + field.visible = field.parentId === selectedItem.id && filteredItemIds.includes(field.id); + field.labelTooltip = `${selectedItem.key.toLowerCase()}:${selectedItem.key}:${field.key}`; + }); + schema.categoryUpdated(selectedItem.id); + }; + + useEffect(() => { + updateVisibleFields(); + }, [filteredItemIds, selectedItem]); + + if(selectedItem?.children) { + return ( + + + {gettext('Navigate to any below item to view or edit its preferences.')} + {selectedItem.children.map((child) => ( + + setSelectedItem(child)} underline="hover">{child.name} + + ))} + + + ); + } + + return ( +
+ { + onDataChange(changedData); + }} + focusOnFirstInput={false} + /> +
+ ); +} +RightPreference.propTypes = { + schema: PropTypes.object.isRequired, + initValues: PropTypes.object.isRequired, + onDataChange: PropTypes.func.isRequired, + filteredItemIds: PropTypes.arrayOf(PropTypes.any).isRequired, + selectedItem: PropTypes.object, + setSelectedItem: PropTypes.func.isRequired, +}; diff --git a/web/pgadmin/browser/server_groups/servers/static/js/binary_path.ui.js b/web/pgadmin/preferences/static/js/components/binary_path.ui.js similarity index 97% rename from web/pgadmin/browser/server_groups/servers/static/js/binary_path.ui.js rename to web/pgadmin/preferences/static/js/components/binary_path.ui.js index c9f361e89..b23adea97 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/binary_path.ui.js +++ b/web/pgadmin/preferences/static/js/components/binary_path.ui.js @@ -11,7 +11,7 @@ import gettext from 'sources/gettext'; import _ from 'lodash'; import url_for from 'sources/url_for'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; -import getApiInstance from '../../../../../static/js/api_instance'; +import getApiInstance from '../../../../static/js/api_instance'; import pgAdmin from 'sources/pgadmin'; export function getBinaryPathSchema() { diff --git a/web/pgadmin/preferences/static/js/components/preferences.ui.js b/web/pgadmin/preferences/static/js/components/preferences.ui.js new file mode 100644 index 000000000..ebc59663e --- /dev/null +++ b/web/pgadmin/preferences/static/js/components/preferences.ui.js @@ -0,0 +1,32 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { BaseUISchema } from '../../../../static/js/SchemaView'; + +export default class PreferencesSchema extends BaseUISchema { + constructor(initValues = {}, schemaFields = []) { + super({ + ...initValues + }); + this.schemaFields = schemaFields; + this.category = ''; + } + + get idAttribute() { + return 'id'; + } + + categoryUpdated() { + this.state?.validate(this.sessData); + } + + get baseFields() { + return this.schemaFields; + } +} diff --git a/web/pgadmin/preferences/static/js/preferences.js b/web/pgadmin/preferences/static/js/preferences.js index ad68f82cb..8176f0e63 100644 --- a/web/pgadmin/preferences/static/js/preferences.js +++ b/web/pgadmin/preferences/static/js/preferences.js @@ -7,11 +7,9 @@ // ////////////////////////////////////////////////////////////// -import React from 'react'; import gettext from 'sources/gettext'; -import PreferencesComponent from './components/PreferencesComponent'; -import PreferencesTree from './components/PreferencesTree'; -import pgAdmin from 'sources/pgadmin'; +import { BROWSER_PANELS } from '../../../browser/static/js/constants'; +import { preferencesPanelData } from '../../../static/js/BrowserComponent'; export default class Preferences { static instance; @@ -48,13 +46,8 @@ export default class Preferences { // This is a callback function to show preferences. show() { - // Render Preferences component - pgAdmin.Browser.notifier.showModal(gettext('Preferences'), (closeModal) => { - return { - // Render preferences tree component - return ; - }} closeModal={closeModal} />; - }, { isFullScreen: false, isResizeable: true, showFullScreen: true, isFullWidth: true, dialogWidth: 900, dialogHeight: 550, id: 'id-preferences' }); + let handler = this.pgBrowser.getDockerHandler?.(BROWSER_PANELS.USER_MANAGEMENT, this.pgBrowser.docker.default_workspace); + handler.focus(); + handler.docker.openTab(preferencesPanelData, BROWSER_PANELS.MAIN, 'middle', true); } } diff --git a/web/pgadmin/static/js/BrowserComponent.jsx b/web/pgadmin/static/js/BrowserComponent.jsx index 63ae4cda9..336355846 100644 --- a/web/pgadmin/static/js/BrowserComponent.jsx +++ b/web/pgadmin/static/js/BrowserComponent.jsx @@ -33,6 +33,7 @@ import pgWindow from 'sources/window'; import WorkspaceToolbar from '../../misc/workspaces/static/js/WorkspaceToolbar'; import { useWorkspace, WorkspaceProvider } from '../../misc/workspaces/static/js/WorkspaceProvider'; import { PgAdminProvider, usePgAdmin } from './PgAdminProvider'; +import PreferencesComponent from '../../preferences/static/js/components/PreferencesComponent'; const objectExplorerGroup = { @@ -45,6 +46,10 @@ export const processesPanelData = { id: BROWSER_PANELS.PROCESSES, title: gettext('Processes'), content: , closable: true, group: 'playground' }; +export const preferencesPanelData = { + id: BROWSER_PANELS.PREFERENCES, title: gettext('Preferences'), content: , closable: true, manualClose: true, group: 'playground' +}; + export const defaultTabsData = [ { id: BROWSER_PANELS.DASHBOARD, title: gettext('Dashboard'), content: , closable: true, group: 'playground' @@ -67,9 +72,11 @@ export const defaultTabsData = [ processesPanelData, ]; -const mainPanelGroup = { - ...getDefaultGroup(), - panelExtra: () => +const getMorePanelGroup = (tabsData) => { + return { + ...getDefaultGroup(), + panelExtra: () => + }; }; let defaultLayout = { @@ -116,7 +123,7 @@ function Layouts({browser}) { savedLayout={pgAdmin.Browser.utils.layout} groups={{ 'object-explorer': objectExplorerGroup, - 'playground': mainPanelGroup, + 'playground': getMorePanelGroup(defaultTabsData), }} noContextGroups={['object-explorer']} resetToTabPanel={BROWSER_PANELS.MAIN} @@ -132,7 +139,7 @@ function Layouts({browser}) { }} defaultLayout={item.layout} groups={{ - 'playground': item?.tabsData ? {...getDefaultGroup(), panelExtra: () => } : {...getDefaultGroup()}, + 'playground': item?.tabsData ? getMorePanelGroup(item?.tabsData) : {...getDefaultGroup()}, }} resetToTabPanel={BROWSER_PANELS.MAIN} isLayoutVisible={currentWorkspace == item.workspace} diff --git a/web/pgadmin/static/js/PgTreeView/index.jsx b/web/pgadmin/static/js/PgTreeView/index.jsx index c79ddad59..3b3084284 100644 --- a/web/pgadmin/static/js/PgTreeView/index.jsx +++ b/web/pgadmin/static/js/PgTreeView/index.jsx @@ -1,38 +1,74 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + import { Checkbox } from '@mui/material'; import { styled } from '@mui/material/styles'; import gettext from 'sources/gettext'; import React, { useEffect, useRef } from 'react'; import { Tree } from 'react-arborist'; -import AutoSizer from 'react-virtualized-auto-sizer'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import PropTypes from 'prop-types'; import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox'; import EmptyPanelMessage from '../components/EmptyPanelMessage'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; - +import useResizeObserver from 'use-resize-observer'; const Root = styled('div')(({ theme }) => ({ height: '100%', - '& .PgTree-tree': { - background: theme.palette.background.default, - height: '100%', - width: '100%', + background: theme.palette.background.default, + width: '100%', + '& *:focus-visible': { + outline: 'none', + }, + '& .PgTree-defaultNode': { display: 'flex', - flexDirection: 'column', - flex: 1, - '& .PgTree-leafNode': { - marginLeft: '1.5rem' + alignItems: 'center', + height: '100%', + flexWrap: 'nowrap', + + '& .PgTree-expandSpacer': { + width: '24px', + height: '24px', + flexShrink: 0, }, - '& .PgTree-node': { - display: 'inline-block', - paddingLeft: '1.5rem', + + '& .PgTree-indentLine': { + width: '24px', + height: '24px', + marginLeft: '-36px', + borderLeft: '1px solid ' + theme.otherVars.borderColor, + flexShrink: 0, + }, + + '& .PgTree-nodeLabel': { height: '100%', + flexGrow: 1, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + '& .PgTree-icon': { + display: 'inline-block', + width: '20px', + backgroundPosition: 'center', + }, + + '& .no-icon': { + display: 'none', + paddingLeft: '0rem', + }, }, - '& .PgTree-focusedNode': { - background: theme.palette.primary.light, - }, + }, + '& .PgTree-focusedNode': { + background: theme.palette.primary.light, }, })); @@ -46,6 +82,7 @@ export default function PgTreeView({ data = [], hasCheckbox = false, const treeObj = useRef(); const treeContainerRef = useRef(); const [selectedCheckBoxNodes, setSelectedCheckBoxNodes] = React.useState([]); + const { ref: containerRef, width, height } = useResizeObserver(); const onSelectionChange = () => { let selectedChNodes = treeObj.current.selectedNodes; @@ -69,31 +106,27 @@ export default function PgTreeView({ data = [], hasCheckbox = false, selectionChange?.(selectedChNodes); }; - return ( + return ( {treeData.length > 0 ? -
treeContainerRef.current = containerRef} className={'PgTree-tree'}> - - {({ width, height }) => ( - { - treeObj.current = obj; - }} - width={isNaN(width) ? 100 : width} - height={isNaN(height) ? 100 : height} - data={treeData} - disableDrag={true} - disableDrop={true} - dndRootElement={treeContainerRef.current} - {...props} - > - { - (props) => - } - - )} - -
+ { + treeObj.current = obj; + }} + width={isNaN(width) ? 100 : width} + height={isNaN(height) ? 100 : height} + data={treeData} + disableDrag={true} + disableDrop={true} + dndRootElement={treeContainerRef.current} + selectionFollowsFocus + {...props} + indent={24} + > + { + (props) => + } +
: @@ -109,7 +142,7 @@ PgTreeView.propTypes = { NodeComponent: PropTypes.func }; -function DefaultNode({ node, style, tree, hasCheckbox, onNodeSelectionChange }) { +function DefaultNode({ node, style, tree, hasCheckbox, onNodeSelectionChange, ...props }) { const pgTreeSelCtx = React.useContext(PgTreeSelectionContext); const [isSelected, setIsSelected] = React.useState(pgTreeSelCtx.includes(node.id) || node.data?.isSelected); @@ -163,28 +196,17 @@ function DefaultNode({ node, style, tree, hasCheckbox, onNodeSelectionChange }) onNodeSelectionChange(); }; - const onSelect = (e) => { - node.focus(); - e.stopPropagation(); - }; - - const onKeyDown = (e) => { - if (e.code == 'Enter') { - onSelect(e); - } - }; - + const className = `${node.isSelected ? 'PgTree-focusedNode' : ''}`; return ( -
- - { - hasCheckbox ? : } - onChange={onCheckboxSelection} /> : - - } -
{node.data.name}
+
+
+ + +
+ + {node.data.name} +
+
); } @@ -197,7 +219,31 @@ DefaultNode.propTypes = { onNodeSelectionChange: PropTypes.func }; -function CollectionArrow({ node, tree, selectedNodeIds }) { +function IndentIcon({node, hasCheckbox, isSelected, isIndeterminate, onCheckboxSelection}) { + if(hasCheckbox) { + return ( + : } + onChange={onCheckboxSelection} + /> + ); + } + if(hasExpand(node)) { + return <>; + } + return
; +} + +IndentIcon.propTypes = { + node: PropTypes.object, + hasCheckbox: PropTypes.bool, + isSelected: PropTypes.bool, + isIndeterminate: PropTypes.bool, + onCheckboxSelection: PropTypes.func +}; + +function ExpandIcon({ node, tree, selectedNodeIds }) { const toggleNode = () => { node.isInternal && node.toggle(); if (node.isSelected && node.isOpen) { @@ -205,14 +251,17 @@ function CollectionArrow({ node, tree, selectedNodeIds }) { selectAllChild(node, tree, 'expand', selectedNodeIds); } }; - return ( - {/* handled by parent */ }}> - {node.isInternal && node?.children.length > 0 ? : null} - - ); + if(hasExpand(node)) { + return ( + {/* handled by parent */ }}> + {node.isOpen ? : } + + ); + } + return (
); } -CollectionArrow.propTypes = { +ExpandIcon.propTypes = { node: PropTypes.object, tree: PropTypes.object, selectedNodeIds: PropTypes.array @@ -251,9 +300,9 @@ function checkAndSelectParent(chNode) { } } -checkAndSelectParent.propTypes = { - chNode: PropTypes.object -}; +function hasExpand(node) { + return node.isInternal && node?.children.length > 0; +} function delectPrentNode(chNode) { if (chNode) { diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index cb7468c44..b17bdd067 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -43,7 +43,8 @@ import { WORKSPACES } from '../../../browser/static/js/constants'; /* If its the dialog */ export default function SchemaDialogView({ getInitData, viewHelperProps, loadingText, schema={}, showFooter=true, - isTabView=true, checkDirtyOnEnableSave=false, customCloseBtnName=gettext('Close'), ...props + isTabView=true, checkDirtyOnEnableSave=false, customCloseBtnName=gettext('Close'), focusOnFirstInput=true, + ...props }) { // View helper properties const onDataChange = props.onDataChange; @@ -190,7 +191,7 @@ export default function SchemaDialogView({ isTabView={isTabView} className={props.formClassName} showError={true} resetKey={resetKey} - focusOnFirstInput={true} + focusOnFirstInput={focusOnFirstInput} /> {showFooter && @@ -268,4 +269,5 @@ SchemaDialogView.propTypes = { Notifier: PropTypes.object, checkDirtyOnEnableSave: PropTypes.bool, customCloseBtnName: PropTypes.string, + focusOnFirstInput: PropTypes.bool, }; diff --git a/web/pgadmin/static/js/components/Buttons.jsx b/web/pgadmin/static/js/components/Buttons.jsx index 972acacb7..f30384254 100644 --- a/web/pgadmin/static/js/components/Buttons.jsx +++ b/web/pgadmin/static/js/components/Buttons.jsx @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////// import { Button, ButtonGroup, Tooltip } from '@mui/material'; -import React, { forwardRef } from 'react'; +import React, { forwardRef, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import CustomPropTypes from '../custom_prop_types'; import ShortcutTitle from './ShortcutTitle'; @@ -174,20 +174,37 @@ DefaultButton.propTypes = { /* pgAdmin Icon button, takes Icon component as input */ -export const PgIconButton = forwardRef(({icon, title, shortcut, className, splitButton, style, color, accesskey, isDropdown, tooltipPlacement, ...props}, ref)=>{ +export const PgIconButton = forwardRef(({icon, title, shortcut, className, splitButton, style, color, isDropdown, tooltipPlacement, ...props}, ref)=>{ + const [tooltipOpen, setTooltipOpen] = useState(false); let shortcutTitle = null; - if(accesskey || shortcut) { - shortcutTitle = ; + if(shortcut) { + shortcutTitle = ; } + useEffect(() => { + // If the button is disabled changes, we should close the tooltip + // as the old button is unmounted. + setTooltipOpen(false); + }, [props.disabled]); + + const tooltipProps = { + title: shortcutTitle || title || '', + 'aria-label': title || '', + open: tooltipOpen, + onOpen: () => setTooltipOpen(true), + onClose: () => setTooltipOpen(false), + enterDelay: isDropdown ? 1500 : undefined, + placement: tooltipPlacement, + }; + if(props.disabled) { if(color == 'primary') { return ( - + + data-label={title || ''} {...props}> {icon} @@ -195,11 +212,11 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, className, split ); } else { return ( - + + data-label={title || ''} {...props}> {icon} @@ -208,10 +225,10 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, className, split } } else if(color == 'primary') { return ( - + + data-label={title || ''} {...props}> {icon} @@ -219,10 +236,10 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, className, split ); } else { return ( - + + data-label={title || ''} {...props}> {icon} diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index ea517f1f1..a40ccbe52 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -1366,6 +1366,7 @@ export function InputTree({hasCheckbox, treeData, onChange, ...props}){ }); return () => umounted = true; }, []); + return <>{isLoading ? : }; } diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 40cfcfa66..0b8b99ab7 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -278,12 +278,12 @@ export function CSVToArray(strData, strDelimiter, quoteChar){ export function hasBinariesConfiguration(pgBrowser, serverInformation) { const module = 'paths'; let preference_name = 'pg_bin_dir'; - let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.'); + let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences.'); if ((serverInformation.type && serverInformation.type === 'ppas') || serverInformation.server_type === 'ppas') { preference_name = 'ppas_bin_dir'; - msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'); + msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences.'); } const preference = usePreferences.getState().getPreferences(module, preference_name); diff --git a/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx b/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx index 8e007d5af..ae5713f38 100644 --- a/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx +++ b/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx @@ -301,7 +301,7 @@ export default function SearchObjects({nodeData}) { if(!rowData.show_node) { setErrorMsg( - gettext('%s objects are disabled in the browser. You can enable them in the preferences dialog.', rowData.type_label)); + gettext('%s objects are disabled in the browser. You can enable them in the preferences.', rowData.type_label)); setTimeout(()=> { document.getElementById('prefdlgid').addEventListener('click', ()=>{ diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py index 647191268..019403532 100644 --- a/web/pgadmin/utils/__init__.py +++ b/web/pgadmin/utils/__init__.py @@ -339,18 +339,18 @@ def does_utility_exist(file): if file is None: error_msg = gettext("Utility file not found. Please correct the Binary" - " Path in the Preferences dialog") + " Path in the Preferences") return error_msg if Path(config.STORAGE_DIR) == Path(file) or \ Path(config.STORAGE_DIR) in Path(file).parents: - error_msg = gettext("Please correct the Binary Path in the Preferences" - " dialog. pgAdmin storage directory can not be a" - " utility binary directory.") + error_msg = gettext("Please correct the Binary Path in the " + "Preferences. pgAdmin storage directory can not " + "be a utility binary directory.") if not os.path.exists(file): error_msg = gettext("'%s' file not found. Please correct the Binary" - " Path in the Preferences dialog" % file) + " Path in the Preferences" % file) return error_msg diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py index c26c71cb0..8d9077d73 100644 --- a/web/pgadmin/utils/preferences.py +++ b/web/pgadmin/utils/preferences.py @@ -49,11 +49,11 @@ class _Preference(): :param label: Display name of the options/preference :param _type: Type for proper validation on value :param default: Default value - :param help_str: Help string to be shown in preferences dialog. + :param help_str: Help string to be shown in preferences. :param min_val: minimum value :param max_val: maximum value :param options: options (Array of list objects) - :param select2: select2 options (object) + :param select: select options (object) :param fields: field schema (if preference has more than one field to take input from user e.g. keyboardshortcut preference) :param allow_blanks: Flag specify whether to allow blank value. @@ -305,7 +305,7 @@ class Preferences(): :param name: Name of the module :param label: Display name of the module, it will be displayed in the - preferences dialog. + preferences. :returns nothing """ @@ -506,7 +506,7 @@ class Preferences(): :param module: Name of the module :param category: Name of category :param name: Name of the option - :param label: Label of the option, shown in the preferences dialog. + :param label: Label of the option, shown in the preferences. :param _type: Type of the option. Allowed type of options are as below: boolean, integer, numeric, date, datetime, diff --git a/web/regression/feature_tests/keyboard_shortcut_test.py b/web/regression/feature_tests/keyboard_shortcut_test.py index 9c7cd23f5..9d11d0b6d 100644 --- a/web/regression/feature_tests/keyboard_shortcut_test.py +++ b/web/regression/feature_tests/keyboard_shortcut_test.py @@ -16,7 +16,8 @@ from selenium.webdriver.common.by import By from selenium.webdriver import ActionChains from selenium.webdriver.common.keys import Keys from regression.feature_utils.base_feature_test import BaseFeatureTest -from regression.feature_utils.locators import NavMenuLocators +from regression.feature_utils.locators import NavMenuLocators, \ + PreferencesLocaltors class KeyboardShortcutFeatureTest(BaseFeatureTest): @@ -88,32 +89,21 @@ class KeyboardShortcutFeatureTest(BaseFeatureTest): NavMenuLocators.preference_menu_item_css) pref_menu_item.click() - self.page.find_by_xpath( - NavMenuLocators.specified_preference_tree_node.format('Browser')) + wait = WebDriverWait(self.page.driver, 10) - display_node = self.page.find_by_xpath( - NavMenuLocators.specified_sub_node_of_pref_tree_node.format( - 'Browser', 'Display')) - attempt = 5 - while attempt > 0: - display_node.click() - # After clicking the element gets loaded in to the dom but still - # not visible, hence sleeping for a sec. - time.sleep(1) - if self.page.wait_for_element_to_be_visible( - self.driver, - NavMenuLocators.show_system_objects_pref_label_xpath, 3): - break - else: - attempt -= 1 + self.page.click_tab("Preferences") - maximize_button = self.page.find_by_css_selector( - NavMenuLocators.maximize_pref_dialogue_css) - maximize_button.click() + # Wait till the preference dialogue box is displayed by checking the + # visibility of Show System Object label + wait.until(EC.presence_of_element_located( + (By.XPATH, + PreferencesLocaltors.show_system_objects_pref_label_xpath)) + ) keyboard_node = self.page.find_by_xpath( - NavMenuLocators.specified_sub_node_of_pref_tree_node.format( - 'Browser', 'Keyboard shortcuts')) + PreferencesLocaltors.specified_preference_tree_node_xpath.format( + 'Keyboard shortcuts')) + keyboard_node.click() for s in self.new_shortcuts: @@ -125,7 +115,11 @@ class KeyboardShortcutFeatureTest(BaseFeatureTest): "input".format(locator)) file_menu.click() + time.sleep(1) file_menu.send_keys(key) # save and close the preference dialog. - self.page.click_modal('Save') + self.page.find_by_css_selector(PreferencesLocaltors.save_btn) \ + .click() + time.sleep(3) + self.page.close_active_tab() diff --git a/web/regression/feature_tests/pg_utilities_backup_restore_test.py b/web/regression/feature_tests/pg_utilities_backup_restore_test.py index c438040ab..b475ab526 100644 --- a/web/regression/feature_tests/pg_utilities_backup_restore_test.py +++ b/web/regression/feature_tests/pg_utilities_backup_restore_test.py @@ -9,16 +9,15 @@ import os -from selenium.common.exceptions import TimeoutException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from regression.feature_utils.base_feature_test import BaseFeatureTest from regression.python_test_utils import test_utils from regression.python_test_utils import test_gui_helper -from regression.feature_utils.locators import NavMenuLocators -from regression.feature_utils.tree_area_locators import TreeAreaLocators -from selenium.webdriver import ActionChains +from regression.feature_utils.locators import NavMenuLocators, \ + PreferencesLocaltors +import time class PGUtilitiesBackupFeatureTest(BaseFeatureTest): @@ -256,26 +255,18 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest): wait = WebDriverWait(self.page.driver, 10) + self.page.click_tab("Preferences") + # Wait till the preference dialogue box is displayed by checking the # visibility of Show System Object label wait.until(EC.presence_of_element_located( - (By.XPATH, NavMenuLocators.show_system_objects_pref_label_xpath)) + (By.XPATH, PreferencesLocaltors. + show_system_objects_pref_label_xpath)) ) - maximize_button = self.page.find_by_css_selector( - NavMenuLocators.maximize_pref_dialogue_css) - maximize_button.click() - - path = self.page.find_by_xpath( - NavMenuLocators.specified_preference_tree_node.format('Paths')) - if self.page.find_by_xpath( - NavMenuLocators.specified_pref_node_exp_status.format('Paths')).\ - get_attribute('aria-expanded') == 'false': - ActionChains(self.driver).double_click(path).perform() - binary_path = self.page.find_by_xpath( - NavMenuLocators.specified_sub_node_of_pref_tree_node.format( - 'Paths', 'Binary paths')) + PreferencesLocaltors.specified_preference_tree_node_xpath. + format('Binary paths')) binary_path.click() default_binary_path = self.server['default_binary_paths'] @@ -313,10 +304,9 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest): # save and close the preference dialog. if path_already_set: - self.page.click_modal('Cancel') + self.page.close_active_tab() else: - self.page.click_modal('Save') - - self.page.wait_for_element_to_disappear( - lambda driver: driver.find_element(By.CSS_SELECTOR, ".ajs-modal") - ) + self.page.find_by_css_selector(PreferencesLocaltors.save_btn) \ + .click() + time.sleep(3) + self.page.close_active_tab() diff --git a/web/regression/feature_tests/test_copy_sql_to_query_tool.py b/web/regression/feature_tests/test_copy_sql_to_query_tool.py index c40a3387d..ff0798068 100644 --- a/web/regression/feature_tests/test_copy_sql_to_query_tool.py +++ b/web/regression/feature_tests/test_copy_sql_to_query_tool.py @@ -13,7 +13,7 @@ from regression.feature_utils.base_feature_test import BaseFeatureTest from regression.python_test_utils import test_utils from regression.feature_utils.tree_area_locators import TreeAreaLocators from regression.feature_utils.locators import NavMenuLocators, \ - QueryToolLocators + QueryToolLocators, PreferencesLocaltors from selenium.webdriver import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By @@ -30,11 +30,10 @@ class CopySQLFeatureTest(BaseFeatureTest): test_table_name = "" def before(self): - + self._update_preferences_setting() self.page.add_server(self.server) def runTest(self): - self._update_preferences_setting() self._create_table() sql_query = self._get_sql_query() query_tool_result = self._get_query_tool_result() @@ -97,44 +96,24 @@ class CopySQLFeatureTest(BaseFeatureTest): NavMenuLocators.file_menu_css) file_menu.click() - self.page.retry_click( - (By.CSS_SELECTOR, NavMenuLocators.preference_menu_item_css), - (By.XPATH, NavMenuLocators.specified_preference_tree_node - .format('Browser')) - ) + pref_menu_item = self.page.find_by_css_selector( + NavMenuLocators.preference_menu_item_css) + pref_menu_item.click() wait = WebDriverWait(self.page.driver, 10) - self.page.retry_click( - (By.XPATH, - NavMenuLocators.specified_sub_node_of_pref_tree_node. - format('Browser', 'Display')), - (By.XPATH, - NavMenuLocators.show_system_objects_pref_label_xpath)) + self.page.click_tab("Preferences") # Wait till the preference dialogue box is displayed by checking the # visibility of Show System Object label wait.until(EC.presence_of_element_located( - (By.XPATH, NavMenuLocators.show_system_objects_pref_label_xpath)) + (By.XPATH, + PreferencesLocaltors.show_system_objects_pref_label_xpath)) ) - maximize_button = self.page.find_by_css_selector( - NavMenuLocators.maximize_pref_dialogue_css) - maximize_button.click() - - specified_preference_tree_node_name = 'Query Tool' - sql_editor = self.page.find_by_xpath( - NavMenuLocators.specified_preference_tree_node.format( - specified_preference_tree_node_name)) - sql_editor.click() - if self.page.find_by_xpath( - NavMenuLocators.specified_pref_node_exp_status. - format(specified_preference_tree_node_name)).get_attribute( - 'aria-expanded') == 'false': - ActionChains(self.driver).double_click(sql_editor).perform() option_node = self.page.find_by_xpath( - "//*[@id='treeContainer']//div//span[text()=" - "'Results grid']//preceding::span[text()='Options'][1]") + "//*[@id='treeContainer']//div//div[text()=" + "'Results grid']//preceding::div[text()='Options'][1]") # self.page.check_if_element_exists_with_scroll(option_node) self.page.driver.execute_script("arguments[0].scrollIntoView(false)", option_node) @@ -147,4 +126,7 @@ class CopySQLFeatureTest(BaseFeatureTest): switch_box_element.click() # save and close the preference dialog. - self.page.click_modal('Save') + self.page.find_by_css_selector(PreferencesLocaltors.save_btn) \ + .click() + time.sleep(3) + self.page.close_active_tab() diff --git a/web/regression/feature_utils/locators.py b/web/regression/feature_utils/locators.py index aa861381e..503fdb07c 100644 --- a/web/regression/feature_utils/locators.py +++ b/web/regression/feature_utils/locators.py @@ -48,22 +48,6 @@ class NavMenuLocators: maintenance_obj_css = "li[data-label='Maintenance...']" - show_system_objects_pref_label_xpath = \ - "//label[contains(text(), 'Show system objects?')]" - - maximize_pref_dialogue_css = "button[data-label='Maximize']" - - maximize_pref_dialogue_css = "button[data-label='Maximize']" - - specified_pref_node_exp_status = \ - "//*[@id='treeContainer']//div//span[text()='{0}']" - - specified_preference_tree_node = \ - "//*[@id='treeContainer']//div//span[text()='{0}']" \ - - specified_sub_node_of_pref_tree_node = \ - "//*[@id='treeContainer']//div//span[text()='{1}']" - insert_bracket_pair_switch_btn = \ ("//div[label[text()='Insert bracket pairs?']]/" "following-sibling::div//input") @@ -112,6 +96,18 @@ class NavMenuLocators: ".btn.btn-sm-sq.btn-primary.pg-bg-close > i" +class PreferencesLocaltors: + show_system_objects_pref_label_xpath = \ + "//label[contains(text(), 'Show system objects?')]" + + specified_preference_tree_node_xpath = \ + ("//*[@id='treeContainer']//div[contains(@class,'PgTree-nodeLabel')]" + "[text()='{0}']") + + save_btn = \ + "#id-preferences button[data-label='Save']" + + class QueryToolLocators: btn_save_file = "button[data-label='Save File']" diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 69e9e6757..3e0c442e0 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -219,6 +219,11 @@ class PgadminPage: else: assert False, "'Tools -> Query Tool' menu did not enable." + def close_active_tab(self): + self.find_by_css_selector(f"div[data-dockid='id-main'] " + ".dock-tab.dock-tab-active " + "button[data-label='Close']").click() + def close_query_tool(self, prompt=True): self.driver.switch_to.default_content() time.sleep(.5) diff --git a/web/regression/javascript/processes/BgProcessManager.spec.js b/web/regression/javascript/processes/BgProcessManager.spec.js index 845eb714b..fe71ff08a 100644 --- a/web/regression/javascript/processes/BgProcessManager.spec.js +++ b/web/regression/javascript/processes/BgProcessManager.spec.js @@ -103,22 +103,4 @@ describe('BgProcessManager', ()=>{ obj.checkPending(); expect(nSpy).toHaveBeenCalled(); }); - - - it('openProcessesPanel', ()=>{ - const panel = {}; - jest.spyOn(pgBrowser.docker.default_workspace, 'openTab').mockReturnValue(panel); - - /* panel open */ - jest.spyOn(pgBrowser.docker.default_workspace, 'find').mockReturnValue(panel); - jest.spyOn(pgBrowser.docker.default_workspace, 'focus'); - obj.openProcessesPanel(); - expect(pgBrowser.docker.default_workspace.focus).toHaveBeenCalled(); - expect(pgBrowser.docker.default_workspace.openTab).not.toHaveBeenCalled(); - - /* panel closed */ - jest.spyOn(pgBrowser.docker.default_workspace, 'find').mockReturnValue(null); - obj.openProcessesPanel(); - expect(pgBrowser.docker.default_workspace.openTab).toHaveBeenCalled(); - }); }); diff --git a/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js b/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js index 6904e1c35..f304ab0d8 100644 --- a/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js @@ -1,4 +1,3 @@ - ///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools @@ -10,8 +9,8 @@ import {genericBeforeEach, getEditView} from '../genericFunctions'; -import {getBinaryPathSchema} from '../../../pgadmin/browser/server_groups/servers/static/js/binary_path.ui'; import pgAdmin from '../fake_pgadmin'; +import { getBinaryPathSchema } from '../../../pgadmin/preferences/static/js/components/binary_path.ui'; describe('BinaryPathschema', ()=>{ diff --git a/web/yarn.lock b/web/yarn.lock index 6f3e572f7..a9bea0e4f 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1413,7 +1413,14 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.27.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.27.4 + resolution: "@babel/runtime@npm:7.27.4" + checksum: 10c0/ca99e964179c31615e1352e058cc9024df7111c829631c90eec84caba6703cc32acc81503771847c306b3c70b815609fe82dde8682936debe295b0b283b2dc6e + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.27.4": version: 7.27.6 resolution: "@babel/runtime@npm:7.27.6" checksum: 10c0/89726be83f356f511dcdb74d3ea4d873a5f0cf0017d4530cb53aa27380c01ca102d573eff8b8b77815e624b1f8c24e7f0311834ad4fb632c90a770fda00bd4c8 @@ -1446,7 +1453,17 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.27.3 + resolution: "@babel/types@npm:7.27.3" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10c0/bafdfc98e722a6b91a783b6f24388f478fd775f0c0652e92220e08be2cc33e02d42088542f1953ac5e5ece2ac052172b3dadedf12bec9aae57899e92fb9a9757 + languageName: node + linkType: hard + +"@babel/types@npm:^7.27.6": version: 7.27.6 resolution: "@babel/types@npm:7.27.6" dependencies: @@ -2290,6 +2307,13 @@ __metadata: languageName: node linkType: hard +"@juggle/resize-observer@npm:^3.3.1": + version: 3.4.0 + resolution: "@juggle/resize-observer@npm:3.4.0" + checksum: 10c0/12930242357298c6f2ad5d4ec7cf631dfb344ca7c8c830ab7f64e6ac11eb1aae486901d8d880fd08fb1b257800c160a0da3aee1e7ed9adac0ccbb9b7c5d93347 + languageName: node + linkType: hard + "@kurkle/color@npm:^0.3.0": version: 0.3.4 resolution: "@kurkle/color@npm:0.3.4" @@ -2598,6 +2622,13 @@ __metadata: languageName: node linkType: hard +"@nozbe/microfuzz@npm:^1.0.0": + version: 1.0.0 + resolution: "@nozbe/microfuzz@npm:1.0.0" + checksum: 10c0/16ce1b36b521f3990b83b08d2a6d1f6eb43fe240d0ebfb600e8f469187a1303c6aa576925b6c0bebee8a8df2f8e8e768e12b1c67d4ac50133468b7a02c46efa9 + languageName: node + linkType: hard + "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -13656,6 +13687,7 @@ __metadata: "@mui/icons-material": "npm:^7.1.1" "@mui/material": "npm:^7.1.0" "@mui/x-date-pickers": "npm:^8.5.0" + "@nozbe/microfuzz": "npm:^1.0.0" "@projectstorm/react-diagrams": "npm:^7.0.4" "@simonwep/pickr": "npm:^1.5.1" "@svgr/webpack": "npm:^8.1.0" @@ -13767,6 +13799,7 @@ __metadata: uplot: "npm:^1.6.32" uplot-react: "npm:^1.1.4" url-loader: "npm:^4.1.1" + use-resize-observer: "npm:^9.1.0" valid-filename: "npm:^4.0.0" vanilla-jsoneditor: "npm:^3.3.1" webfonts-loader: "npm:^8.0.1" @@ -15703,6 +15736,18 @@ __metadata: languageName: node linkType: hard +"use-resize-observer@npm:^9.1.0": + version: 9.1.0 + resolution: "use-resize-observer@npm:9.1.0" + dependencies: + "@juggle/resize-observer": "npm:^3.3.1" + peerDependencies: + react: 16.8.0 - 18 + react-dom: 16.8.0 - 18 + checksum: 10c0/6ccdeb09fe20566ec182b1635a22f189e13d46226b74610432590e69b31ef5d05d069badc3306ebd0d2bb608743b17981fb535763a1d7dc2c8ae462ee8e5999c + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.2.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0"