1) Open preferences in a new tab instead of a dialog for better user experience. #6743

2) Add a search box to enable searching within the preferences tab. #2864
pull/8848/head
Aditya Toshniwal 2025-06-12 19:03:54 +05:30 committed by GitHub
parent 814250aade
commit 1e0e9c4f7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1127 additions and 1090 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -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.

View File

@ -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",

View File

@ -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'},

View File

@ -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:[

View File

@ -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

View File

@ -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:[{

View File

@ -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'],

View File

@ -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',

View File

@ -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) {

View File

@ -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 (
<Resizable className='PreferencesComponent-treeContainer'
enable={{ top:false, right:true, bottom:false, left:false, topRight:false, bottomRight:false, bottomLeft:false, topLeft:false }}
maxWidth='50%' minWidth='10%'
defaultSize={{ width: '25%', height: '100%' }}
id='treeContainer'
>
<PgTreeView
idAccessor='id'
data={filteredTreeData}
openByDefault={true}
disableMultiSelection={true}
selection={selectedItem?.id}
onFocus={(item) => {
setSelectedItem(item.data);
}}
// don't need virtualization for preferences tree
overscanCount={50}
/>
</Resizable>
);
}
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,
};

View File

@ -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.')}<br><br>${gettext('Do you want to proceed?')}<br><br>
${gettext('Note:')}<br> <ul style="padding-left:20px"><li style="list-style-type:disc">${gettext('The object explorer tree will be refreshed automatically to reflect the changes.')}</li>
<li style="list-style-type:disc">${gettext('If the application language changes, a reload of the application will be required. You can choose to reload later at your convenience.')}</li></ul>`;
return (
<StyledBox display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>
{HTMLReactParser(text)}
</Box>
<Box className='Alert-footer'>
<DefaultButton className='Alert-margin' startIcon={<CloseIcon />} onClick={modalClose}>
{gettext('Cancel')}
</DefaultButton>
<DefaultButton className='Alert-margin' startIcon={<SaveSharpIcon />} onClick={() => handleResetClick(true)}>
{gettext('Save & Reload')}
</DefaultButton>
<PrimaryButton className='Alert-margin' startIcon={<SaveSharpIcon />} onClick={() => handleResetClick(false)}>
{gettext('Save & Reload Later')}
</PrimaryButton>
</Box>
</StyledBox>
);
},
{ isFullScreen: false, isResizeable: false, showFullScreen: false, isFullWidth: false, showTitle: true, id: 'id-reset-preferences' }
);
}

View File

@ -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 (<FileTreeX model={pTreeModelX.current} height={'100%'} onReady={onReadyRef.current} />);
}
PreferencesTree.propTypes = {
pgBrowser: PropTypes.any,
data: PropTypes.array,
ptree: PropTypes.any,
};

View File

@ -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 (
<Box className='PreferencesComponent-preferencesContainer'>
<Box className='PreferencesComponent-noSelection'>
<Box>{gettext('Navigate to any below item to view or edit its preferences.')}</Box>
{selectedItem.children.map((child) => (
<Box key={child.id}>
<Link component='button' onClick={()=>setSelectedItem(child)} underline="hover">{child.name}</Link>
</Box>
))}
</Box>
</Box>
);
}
return (
<div className='PreferencesComponent-preferencesContainer' ref={schemaViewRef}>
<SchemaView
key={selectedItem?.id ?? 0}
formType={'dialog'}
getInitData={getInitData}
viewHelperProps={{ mode: 'edit' }}
schema={schema}
showFooter={false}
isTabView={false}
formClassName='PreferencesComponent-preferencesContainerBackground'
onDataChange={(isChanged, changedData) => {
onDataChange(changedData);
}}
focusOnFirstInput={false}
/>
</div>
);
}
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,
};

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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 <PreferencesComponent
renderTree={(prefTreeData) => {
// Render preferences tree component
return <PreferencesTree pgBrowser={this.pgBrowser} data={prefTreeData} />;
}} 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);
}
}

View File

@ -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: <Processes />, closable: true, group: 'playground'
};
export const preferencesPanelData = {
id: BROWSER_PANELS.PREFERENCES, title: gettext('Preferences'), content: <PreferencesComponent panelId={BROWSER_PANELS.PREFERENCES} />, closable: true, manualClose: true, group: 'playground'
};
export const defaultTabsData = [
{
id: BROWSER_PANELS.DASHBOARD, title: gettext('Dashboard'), content: <Dashboard />, closable: true, group: 'playground'
@ -67,9 +72,11 @@ export const defaultTabsData = [
processesPanelData,
];
const mainPanelGroup = {
...getDefaultGroup(),
panelExtra: () => <MainMoreToolbar tabsData={defaultTabsData}/>
const getMorePanelGroup = (tabsData) => {
return {
...getDefaultGroup(),
panelExtra: () => <MainMoreToolbar tabsData={tabsData}/>
};
};
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: () => <MainMoreToolbar tabsData={item.tabsData}/>} : {...getDefaultGroup()},
'playground': item?.tabsData ? getMorePanelGroup(item?.tabsData) : {...getDefaultGroup()},
}}
resetToTabPanel={BROWSER_PANELS.MAIN}
isLayoutVisible={currentWorkspace == item.workspace}

View File

@ -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 (<Root>
return (<Root ref={containerRef} className={'PgTree-tree'}>
{treeData.length > 0 ?
<PgTreeSelectionContext.Provider value={selectedCheckBoxNodes}>
<div ref={(containerRef) => treeContainerRef.current = containerRef} className={'PgTree-tree'}>
<AutoSizer>
{({ width, height }) => (
<Tree
ref={(obj) => {
treeObj.current = obj;
}}
width={isNaN(width) ? 100 : width}
height={isNaN(height) ? 100 : height}
data={treeData}
disableDrag={true}
disableDrop={true}
dndRootElement={treeContainerRef.current}
{...props}
>
{
(props) => <Node onNodeSelectionChange={onSelectionChange} hasCheckbox={hasCheckbox} {...props} />
}
</Tree>
)}
</AutoSizer>
</div>
<Tree
ref={(obj) => {
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) => <Node onNodeSelectionChange={onSelectionChange} hasCheckbox={hasCheckbox} {...props} />
}
</Tree>
</PgTreeSelectionContext.Provider>
:
<EmptyPanelMessage text={gettext('No objects are found to display')} />
@ -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 (
<div style={style} className={node.isFocused ? 'PgTree-focusedNode' : ''} onClick={onSelect} onKeyDown={onKeyDown}>
<CollectionArrow node={node} tree={tree} selectedNodeIds={pgTreeSelCtx} />
{
hasCheckbox ? <Checkbox style={{ padding: 0 }} color="primary" className={!node.isInternal ? 'PgTree-leafNode' : null}
checked={isSelected}
checkedIcon={isIndeterminate ? <IndeterminateCheckBoxIcon style={{ height: '1.4rem' }} /> : <CheckBoxIcon style={{ height: '1.4rem' }} />}
onChange={onCheckboxSelection} /> :
<span className={node.data.icon}></span>
}
<div className={node.data.icon + ' PgTree-node'}>{node.data.name}</div>
<div style={style} className={className} {...props}>
<div className={'PgTree-defaultNode'}>
<ExpandIcon node={node} tree={tree} selectedNodeIds={pgTreeSelCtx} />
<IndentIcon node={node} hasCheckbox={hasCheckbox} isSelected={isSelected} isIndeterminate={isIndeterminate} onCheckboxSelection={onCheckboxSelection} />
<div className='PgTree-nodeLabel'>
<span className={`PgTree-icon ${node.data.icon || 'no-icon'}`} />
{node.data.name}
</div>
</div>
</div>
);
}
@ -197,7 +219,31 @@ DefaultNode.propTypes = {
onNodeSelectionChange: PropTypes.func
};
function CollectionArrow({ node, tree, selectedNodeIds }) {
function IndentIcon({node, hasCheckbox, isSelected, isIndeterminate, onCheckboxSelection}) {
if(hasCheckbox) {
return (
<Checkbox style={{ padding: 0 }} color="primary"
checked={isSelected}
checkedIcon={isIndeterminate ? <IndeterminateCheckBoxIcon style={{ height: '1.5rem' }} /> : <CheckBoxIcon style={{ height: '1.5rem' }} />}
onChange={onCheckboxSelection}
/>
);
}
if(hasExpand(node)) {
return <></>;
}
return <div className='PgTree-indentLine'></div>;
}
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 (
<span onClick={toggleNode} onKeyDown={() => {/* handled by parent */ }}>
{node.isInternal && node?.children.length > 0 ? <ToggleArrowIcon node={node} /> : null}
</span>
);
if(hasExpand(node)) {
return (
<span onClick={toggleNode} onKeyDown={() => {/* handled by parent */ }}>
{node.isOpen ? <ExpandMoreIcon /> : <ChevronRightIcon />}
</span>
);
}
return (<div className='PgTree-expandSpacer'></div>);
}
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) {

View File

@ -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}
/>
</Box>
{showFooter &&
@ -268,4 +269,5 @@ SchemaDialogView.propTypes = {
Notifier: PropTypes.object,
checkDirtyOnEnableSave: PropTypes.bool,
customCloseBtnName: PropTypes.string,
focusOnFirstInput: PropTypes.bool,
};

View File

@ -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 = <ShortcutTitle title={title} accesskey={accesskey} shortcut={shortcut}/>;
if(shortcut) {
shortcutTitle = <ShortcutTitle title={title} shortcut={shortcut}/>;
}
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 (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''} enterDelay={isDropdown ? 1500 : undefined} placement={tooltipPlacement}>
<Tooltip {...tooltipProps}>
<span>
<PrimaryButton ref={ref} style={style}
className={['Buttons-iconButton', (splitButton ? 'Buttons-splitButton' : ''), className].join(' ')}
accessKey={accesskey} data-label={title || ''} {...props}>
data-label={title || ''} {...props}>
{icon}
</PrimaryButton>
</span>
@ -195,11 +212,11 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, className, split
);
} else {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''} enterDelay={isDropdown ? 1500 : undefined} placement={tooltipPlacement}>
<Tooltip {...tooltipProps}>
<span>
<DefaultButton ref={ref} style={style}
className={['Buttons-iconButton', 'Buttons-iconButtonDefault',(splitButton ? 'Buttons-splitButton' : ''), className].join(' ')}
accessKey={accesskey} data-label={title || ''} {...props}>
data-label={title || ''} {...props}>
{icon}
</DefaultButton>
</span>
@ -208,10 +225,10 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, className, split
}
} else if(color == 'primary') {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''} enterDelay={isDropdown ? 1500 : undefined} placement={tooltipPlacement}>
<Tooltip {...tooltipProps}>
<PrimaryButton ref={ref} style={style}
className={['Buttons-iconButton', (splitButton ? 'Buttons-splitButton' : ''), className].join(' ')}
accessKey={accesskey} data-label={title || ''} {...props}>
data-label={title || ''} {...props}>
{icon}
</PrimaryButton>
</Tooltip>
@ -219,10 +236,10 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, className, split
);
} else {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''} enterDelay={isDropdown ? 1500 : undefined} placement={tooltipPlacement}>
<Tooltip {...tooltipProps}>
<DefaultButton ref={ref} style={style}
className={['Buttons-iconButton', 'Buttons-iconButtonDefault',(splitButton ? 'Buttons-splitButton' : ''), className].join(' ')}
accessKey={accesskey} data-label={title || ''} {...props}>
data-label={title || ''} {...props}>
{icon}
</DefaultButton>
</Tooltip>

View File

@ -1366,6 +1366,7 @@ export function InputTree({hasCheckbox, treeData, onChange, ...props}){
});
return () => umounted = true;
}, []);
return <>{isLoading ? <Loader message={gettext('Loading')}></Loader> : <PgTreeView data={finalData} hasCheckbox={hasCheckbox} selectionChange={onChange} {...props}></PgTreeView>}</>;
}

View File

@ -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);

View File

@ -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 <a id="prefdlgid" class="pref-dialog-link">preferences dialog</a>.', rowData.type_label));
gettext('%s objects are disabled in the browser. You can enable them in the <a id="prefdlgid" class="pref-dialog-link">preferences</a>.', rowData.type_label));
setTimeout(()=> {
document.getElementById('prefdlgid').addEventListener('click', ()=>{

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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']"

View File

@ -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)

View File

@ -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();
});
});

View File

@ -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', ()=>{

View File

@ -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"