Fixes for the preferences dialog

1) Add server mode validation in the binary path.
  2) Updated preferences tree rendering to avoid using the ReactDOM render.
  3) Updated CSS for keyboard shortcuts checkbox border makes it consistent with input box border.
  4) Fixed jasmine test case and improved code coverage.
  5) Fixed SonarQube issues.
  6) Added validation to disable "Maximum column with" option if "Column sized by" option is set to "Column name" in Query Tool -> Result grid.
  7) Updated documentation with the latest screenshots.
  8) Correct typo in the documentation. Fixes #7261

refs #7149
This commit is contained in:
Nikhil Mohite
2022-03-23 13:28:35 +05:30
committed by Akshay Joshi
parent 1711834229
commit 2f37f0ca51
24 changed files with 338 additions and 330 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -421,7 +421,7 @@ Query Tool window navigation:
Use the fields on the *SQL formatting* panel to specify your preferences for
reformatting of SQL.
* Use the *Command-first notation* option to specify whether to place commas
* Use the *Comma-first notation* option to specify whether to place commas
before or after column names.
* Use the *Identifier case* option to specify whether to change identifiers
(object names) into upper, lower, or capitalized case.

View File

@@ -22,4 +22,5 @@ Bug fixes
| `Issue #7059 <https://redmine.postgresql.org/issues/7059>`_ - Fixed an issue where the error is shown on logout when the authentication source is oauth2.
| `Issue #7216 <https://redmine.postgresql.org/issues/7216>`_ - Ensure that the values of certain fields are prettified in the statistics tab for collection nodes.
| `Issue #7238 <https://redmine.postgresql.org/issues/7238>`_ - Fixed an issue where foreign key is not removed even if the referred table is removed in ERD.
| `Issue #7257 <https://redmine.postgresql.org/issues/7257>`_ - Support running the container under OpenShift with alternate UIDs.
| `Issue #7257 <https://redmine.postgresql.org/issues/7257>`_ - Support running the container under OpenShift with alternate UIDs.
| `Issue #7261 <https://redmine.postgresql.org/issues/7261>`_ - Correct typo in the documentation.

View File

@@ -26,7 +26,7 @@ click on a result row to select the object in the
:ref:`browser <tree_control>`. If the object is greyed out, this means that you
have not enabled those object types in the :ref:`preferences <preferences>`,
so you can't double click on it. You can click on the ellipsis appended to
the function and procedure names to see there arguments.
the function and procedure names to see their arguments.
You can filter based on a particular object type by selecting one from the

View File

@@ -13,6 +13,7 @@ import url_for from 'sources/url_for';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import getApiInstance from '../../../../../static/js/api_instance';
import Notify from '../../../../../static/js/helpers/Notifier';
import pgAdmin from 'sources/pgadmin';
export function getBinaryPathSchema() {
@@ -49,6 +50,7 @@ export default class BinaryPathSchema extends BaseUISchema {
{
id: 'binaryPath', label: gettext('Binary Path'), cell: 'file', type: 'file',
isvalidate: true, controlProps: { dialogType: 'select_folder', supportedTypes: ['*', 'sql', 'backup'], dialogTitle: 'Select folder' },
hideBrowseButton: pgAdmin.server_mode == 'True',
validate: (data) => {
const api = getApiInstance();
if (_.isNull(data) || data.trim() === '') {

View File

@@ -8,8 +8,9 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import _ from 'lodash';
import url_for from 'sources/url_for';
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { FileType } from 'react-aspen';
import { Box } from '@material-ui/core';
import PropTypes from 'prop-types';
@@ -209,100 +210,8 @@ export default function PreferencesComponent({ ...props }) {
'expanded': false,
};
if (subNode.label == 'Nodes' && node.label == 'Browser') {
//Add Note for Nodes
preferencesData.push(
{
id: 'note_' + subNode.id,
type: 'note', text: [gettext('This settings is to Show/Hide nodes in the browser tree.')].join(''),
visible: false,
'parentId': nodeData['id']
},
);
}
subNode.preferences.forEach((element) => {
let addNote = false;
let note = '';
let type = getControlMappedForType(element.type);
if (type === 'file') {
addNote = true;
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);
}
else if (type == 'select') {
if (element.control_props !== undefined) {
element.controlProps = element.control_props;
} else {
element.controlProps = {};
}
element.type = type;
preferencesValues[element.id] = element.value;
if (element.name == 'theme') {
element.type = 'theme';
element.options.forEach((opt) => {
if (opt.value == element.value) {
opt.selected = true;
} else {
opt.selected = false;
}
});
}
}
else if (type === 'keyboardShortcut') {
element.type = 'keyboardShortcut';
element.canAdd = false;
element.canDelete = false;
element.canEdit = false;
element.editable = false;
if (pgAdmin.Browser.get_preference(node.label.toLowerCase(), element.name)?.value) {
let temp = pgAdmin.Browser.get_preference(node.label.toLowerCase(), element.name).value;
preferencesValues[element.id] = temp;
} else {
preferencesValues[element.id] = element.value;
}
delete element.value;
} else if (type === 'threshold') {
element.type = 'threshold';
let _val = element.value.split('|');
preferencesValues[element.id] = { 'warning': _val[0], 'alert': _val[1] };
} 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);
if (addNote) {
preferencesData.push(
{
id: 'note_' + element.id,
type: 'note', text: [
'<ul><li>',
gettext(note),
'</li></ul>',
].join(''),
visible: false,
'parentId': nodeData['id']
},
);
}
element.parentId = nodeData['id'];
});
addNote(node, subNode, nodeData, preferencesData);
setPreferences(node, subNode, nodeData, preferencesValues, preferencesData);
tdata['childrenNodes'].push(nodeData);
});
@@ -318,11 +227,128 @@ export default function PreferencesComponent({ ...props }) {
Notify.alert(err);
});
}, []);
function setPreferences(node, subNode, nodeData, preferencesValues, preferencesData) {
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);
addNote(node, subNode, nodeData, preferencesData, note);
}
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 == 'Results grid' && node.label == 'Query Tool') {
setResultsOptions(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';
};
}
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;
}
});
}
}
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 (pgAdmin.Browser.get_preference(node.label.toLowerCase(), element.name)?.value) {
let temp = pgAdmin.Browser.get_preference(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 == 'Nodes' && node.label == 'Browser') {
note = [gettext('This settings is to Show/Hide nodes in the browser tree.')].join('');
} else {
note = [gettext(note)].join('');
}
if (note && note.length > 0) {
//Add Note for Nodes
preferencesData.push(
{
id: _.uniqueId('note') + subNode.id,
type: 'note', text: note,
visible: false,
'parentId': nodeData['id']
},
);
}
}
useEffect(() => {
props.renderTree(prefTreeData);
let initTreeTimeout = null;
// Listen selected preferences tree node event and show the appropriate components in right panel.
pgAdmin.Browser.Events.on('preferences:tree:selected', (item) => {
if (item.type == FileType.File) {
@@ -330,12 +356,12 @@ export default function PreferencesComponent({ ...props }) {
field.visible = field.parentId === item._metadata.data.id;
});
setLoadTree(Math.floor(Math.random() * 1000));
initTreeTimeout = setTimeout(()=> {
initTreeTimeout = setTimeout(() => {
prefTreeInit.current = true;
}, 10);
}
else {
if(item.isExpanded && item._children && item._children.length > 0 && prefTreeInit.current) {
if (item.isExpanded && item._children && item._children.length > 0 && prefTreeInit.current) {
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true);
}
}
@@ -343,29 +369,24 @@ export default function PreferencesComponent({ ...props }) {
// Listen open preferences tree node event to default select first child node on parent node selection.
pgAdmin.Browser.Events.on('preferences:tree:opened', (item) => {
if (item._fileName == 'Browser' && item.type == 2 && item.isExpanded && item._children && item._children.length > 0 && !prefTreeInit.current) {
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], false);
}
else if(prefTreeInit.current) {
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true);
}
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', (item) => {
// Check the if newely added node is Directoy call toggle to expand the node.
if (item.type == FileType.Directory) {
if (item._parent._fileName == 'Browser' && item._parent.isExpanded && !prefTreeInit.current) {
pgAdmin.Browser.ptree.tree.setActiveFile(item._parent._children[0], false);
}
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);
}
});
/* Clear the initTreeTimeout timeout if unmounted */
return ()=>{
return () => {
clearTimeout(initTreeTimeout);
};
}, [prefTreeData]);
}, []);
function getControlMappedForType(type) {
switch (type) {
@@ -414,7 +435,7 @@ export default function PreferencesComponent({ ...props }) {
}
}
function getCollectionValue(_metadata, value, initValues) {
function getCollectionValue(_metadata, value, initVals) {
let val = value;
if (typeof (value) == 'object') {
if (_metadata[0].type == 'collection' && _metadata[0].schema) {
@@ -424,14 +445,7 @@ export default function PreferencesComponent({ ...props }) {
value.changed.forEach((chValue) => {
pathVersions.push(chValue.version);
});
initValues[_metadata[0].id].forEach((initVal) => {
if (pathVersions.includes(initVal.version)) {
pathData.push(value.changed[pathVersions.indexOf(initVal.version)]);
}
else {
pathData.push(initVal);
}
});
getPathData(initVals, pathData, _metadata, value, pathVersions);
val = JSON.stringify(pathData);
} else {
let key_val = {
@@ -450,12 +464,23 @@ export default function PreferencesComponent({ ...props }) {
return val;
}
function savePreferences(data, initValues) {
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, initValues);
let val = getCollectionValue(_metadata, value, initVal);
_data.push({
'category_id': _metadata[0]['cid'],
'id': parseInt(key),
@@ -536,14 +561,14 @@ export default function PreferencesComponent({ ...props }) {
location.reload();
return true;
},
function () { props.closeModal(); /*props.panel.close()*/ },
function () { props.closeModal();},
gettext('Refresh'),
gettext('Later')
);
}
// Refresh preferences cache
pgAdmin.Browser.cache_preferences(modulesChanged);
props.closeModal(); /*props.panel.close()*/
props.closeModal();
}).catch((err) => {
Notify.alert(err.response.data);
});
@@ -558,7 +583,12 @@ export default function PreferencesComponent({ ...props }) {
<Box className={classes.root}>
<Box className={clsx(classes.preferences)}>
<Box className={clsx(classes.treeContainer)} >
<Box className={clsx('aciTree', classes.tree)} id={'treeContainer'}></Box>
<Box className={clsx('aciTree', classes.tree)} id={'treeContainer'}>
{
useMemo(() => (prefTreeData && props.renderTree(prefTreeData)), [prefTreeData])
}
</Box>
</Box>
<Box className={clsx(classes.preferencesContainer)}>
{
@@ -576,7 +606,7 @@ export default function PreferencesComponent({ ...props }) {
<PgIconButton data-test="dialog-help" onClick={onDialogHelp} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
</Box>
<Box className={classes.actionBtn} marginLeft="auto">
<DefaultButton className={classes.buttonMargin} onClick={() => { props.closeModal(); /*props.panel.close()*/ }} startIcon={<CloseSharpIcon onClick={() => { props.closeModal(); /*props.panel.close()*/ }} />}>
<DefaultButton className={classes.buttonMargin} onClick={() => { props.closeModal();}} startIcon={<CloseSharpIcon onClick={() => { props.closeModal();}} />}>
{gettext('Cancel')}
</DefaultButton>
<PrimaryButton className={classes.buttonMargin} startIcon={<SaveSharpIcon />} disabled={disableSave} onClick={() => { savePreferences(prefChangedData, initValues); }}>
@@ -584,8 +614,6 @@ export default function PreferencesComponent({ ...props }) {
</PrimaryButton>
</Box>
</Box>
{/* </Box> */}
</Box >
</Box>
);

View File

@@ -7,62 +7,79 @@
// //
// //////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import * as React from 'react';
import { render } from 'react-dom';
import { Directory } from 'react-aspen';
import PropTypes from 'prop-types';
import { Directory} from 'react-aspen';
import { FileTreeX, TreeModelX } from 'pgadmin4-tree';
import {Tree} from '../../../../static/js/tree/tree';
import { Tree } from '../../../../static/js/tree/tree';
import { ManagePreferenceTreeNodes } from '../../../../static/js/tree/preference_nodes';
import pgAdmin from 'sources/pgadmin';
var initPreferencesTree = async (pgBrowser, containerElement, data) => {
export default function PreferencesTree({ pgBrowser, data }) {
const pTreeModelX = React.useRef();
const onReadyRef = React.useRef();
const [loaded, setLoaded] = React.useState(false);
const MOUNT_POINT = '/preferences';
// Setup host
let ptree = new ManagePreferenceTreeNodes(data);
React.useEffect(() => {
setLoaded(false);
// Init Tree with the Tree Parent node '/browser'
ptree.init(MOUNT_POINT);
// Setup host
let ptree = new ManagePreferenceTreeNodes(data);
// Init Tree with the Tree Parent node '/browser'
ptree.init(MOUNT_POINT);
const host = {
pathStyle: 'unix',
getItems: async (path) => {
return ptree.readNode(path);
},
sortComparator: (a, b) => {
// No nee to sort Query tool options.
if (a._parent && a._parent._fileName == '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;
},
};
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 == '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;
},
};
const pTreeModelX = new TreeModelX(host, MOUNT_POINT);
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;
};
const itemHandle = function onReady(handler) {
// Initialize preferences Tree
pgBrowser.ptree = new Tree(handler, ptree, pgBrowser, 'preferences');
return true;
};
pTreeModelX.current.root.ensureLoaded().then(() => {
setLoaded(true);
});
}, [data]);
await pTreeModelX.root.ensureLoaded();
if (!loaded || _.isUndefined(pTreeModelX.current) || _.isUndefined(onReadyRef.current)) {
return (gettext('Loading...'));
}
return (<FileTreeX model={pTreeModelX.current} height={'100%'} onReady={onReadyRef.current} />);
}
// Render Browser Tree
await render(
<FileTreeX model={pTreeModelX} height={'100%'}
onReady={itemHandle} />
, containerElement);
};
module.exports = {
initPreferencesTree: initPreferencesTree,
PreferencesTree.propTypes = {
pgBrowser: PropTypes.any,
data: PropTypes.array,
ptree: PropTypes.any,
};

View File

@@ -11,8 +11,7 @@ import React from 'react';
import gettext from 'sources/gettext';
import PreferencesComponent from './components/PreferencesComponent';
import Notify from '../../../static/js/helpers/Notifier';
// import PreferencesTree from './components/PreferencesTree';
import { initPreferencesTree } from './components/PreferencesTree';
import PreferencesTree from './components/PreferencesTree';
export default class Preferences {
static instance;
@@ -50,12 +49,15 @@ export default class Preferences {
// This is a callback function to show preferences.
show() {
// Render Preferences component
Notify.showModal(gettext('Preferences'), (closeModal) => {
return <PreferencesComponent
renderTree={(prefTreeData) => {
initPreferencesTree(this.pgBrowser, document.getElementById('treeContainer'), 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 });
}
}

View File

@@ -12,7 +12,7 @@ import _ from 'lodash';
import {
FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString,
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputThemes, InputRadio
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio
} from '../components/FormComponents';
import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils';
@@ -85,7 +85,7 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible,
case 'threshold':
return <FormInputQueryThreshold name={name} value={value} onChange={onTextChange} {...props}/>;
case 'theme':
return <FormInputThemes name={name} value={value} onChange={onTextChange} {...props}/>;
return <FormInputSelectThemes name={name} value={value} onChange={onTextChange} {...props}/>;
default:
return <PlainString value={value} {...props} />;
}
@@ -197,7 +197,7 @@ const ALLOWED_PROPS_FIELD_COMMON = [
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
'orientation', 'isvalidate', 'fields', 'radioType'
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton'
];
const ALLOWED_PROPS_FIELD_FORM = [
@@ -205,7 +205,7 @@ const ALLOWED_PROPS_FIELD_FORM = [
];
const ALLOWED_PROPS_FIELD_CELL = [
'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType'
'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType', 'hideBrowseButton'
];

View File

@@ -33,6 +33,6 @@ export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{heigh
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;
export const ExpandDialog = ()=><ExternalIcon Icon={Expand} style={{height: '1em', width: '1em'}} />;
export const MinimizeDialog = ()=><ExternalIcon Icon={Collapse} style={{height: 'auto'}} />;
export const ExpandDialogIcon = ()=><ExternalIcon Icon={Expand} style={{height: 'auto', width: '1em'}} />;
export const MinimizeDialogIcon = ()=><ExternalIcon Icon={Collapse} style={{height: 'auto'}} />;

View File

@@ -41,7 +41,7 @@ import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
import CustomPropTypes from '../custom_prop_types';
import KeyboardShortcuts from './KeyboardShortcuts';
import QueryThresholds from './QueryThresholds';
import Themes from './Themes';
import SelectThemes from './SelectThemes';
const useStyles = makeStyles((theme) => ({
@@ -405,7 +405,7 @@ FormInputText.propTypes = {
};
/* Using the existing file dialog functions using showFileDialog */
export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, validate, ...props }) {
export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, hideBrowseButton=false,validate, ...props }) {
const inpRef = useRef();
const onFileSelect = (value) => {
onChange && onChange(decodeURI(value));
@@ -414,8 +414,10 @@ export function InputFileSelect({ controlProps, onChange, disabled, readonly, is
return (
<InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} {...props} endAdornment={
<>
{!hideBrowseButton &&
<IconButton onClick={() => showFileDialog(controlProps, onFileSelect)}
disabled={disabled || readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
}
{isvalidate &&
<PgIconButton title={gettext('Validate')} style={{ border: 'none' }} disabled={!props.value} onClick={() => { validate(props.value); }} icon={<AssignmentTurnedIn />}></PgIconButton>
}
@@ -430,7 +432,8 @@ InputFileSelect.propTypes = {
readonly: PropTypes.bool,
isvalidate: PropTypes.bool,
validate: PropTypes.func,
value: PropTypes.string
value: PropTypes.string,
hideBrowseButton: PropTypes.bool
};
export function FormInputFileSelect({
@@ -510,7 +513,6 @@ export function InputCheckbox({ cid, helpid, value, onChange, controlProps, read
{...props} />
}
label={controlProps.label}
labelPlacement={props?.labelPlacement ? props.labelPlacement : 'end'}
/>
);
}
@@ -521,7 +523,6 @@ InputCheckbox.propTypes = {
controlProps: PropTypes.object,
onChange: PropTypes.func,
readonly: PropTypes.bool,
labelPlacement: PropTypes.string
};
export function FormInputCheckbox({ hasError, required, label,
@@ -1189,13 +1190,11 @@ const useStylesKeyboardShortcut = makeStyles(() => ({
}
}));
export function FormInputKeyboardShortcut({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
const cid = _.uniqueId('c');
const helpid = `h${cid}`;
export function FormInputKeyboardShortcut({ hasError, label, className, helpMessage, onChange, ...props }) {
const classes = useStylesKeyboardShortcut();
return (
<FormInput label={label} error={hasError} className={clsx(classes.customRow, className)} helpMessage={helpMessage} testcid={testcid}>
<KeyboardShortcuts cid={cid} helpid={helpid} onChange={onChange} {...props} />
<FormInput label={label} error={hasError} className={clsx(classes.customRow, className)} helpMessage={helpMessage}>
<KeyboardShortcuts onChange={onChange} {...props} />
</FormInput>
);
@@ -1229,17 +1228,17 @@ FormInputQueryThreshold.propTypes = {
};
export function FormInputThemes({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
export function FormInputSelectThemes({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
const cid = _.uniqueId('c');
const helpid = `h${cid}`;
return (
<FormInput label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<Themes cid={cid} helpid={helpid} onChange={onChange} {...props} />
<SelectThemes cid={cid} helpid={helpid} onChange={onChange} {...props} />
</FormInput>
);
}
FormInputThemes.propTypes = {
FormInputSelectThemes.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,

View File

@@ -22,6 +22,7 @@ const useStyles = makeStyles((theme) => ({
inputCheckboxClass: {
border: '1px solid',
borderRadius: theme.shape.borderRadius,
borderColor: theme.otherVars.inputBorderColor,
padding: 3
}
}));
@@ -36,6 +37,7 @@ export default function KeyboardShortcuts({ value, onChange, fields }) {
const ctrlhelpid = `h${ctrlCid}`;
const altCid = _.uniqueId('c');
const althelpid = `h${altCid}`;
const keyLabel = _.uniqueId('c');
const onKeyDown = (e) => {
let newVal = { ...value };
@@ -72,19 +74,20 @@ export default function KeyboardShortcuts({ value, onChange, fields }) {
<Grid
container
direction="row"
justifyContent="center"
alignItems="center">
alignItems="center"
key={_.uniqueId('c')}
>
{fields.map(element => {
let ctrlProps = {
label: element.label
};
if (element.type == 'keyCode') {
return <Grid item container lg={4} md={4} sm={4} xs={12}>
return <Grid item container lg={4} md={4} sm={4} xs={12} key={_.uniqueId('c')}>
<Grid item lg={4} md={4} sm={4} xs={12} className={classes.inputLabel}>
<Typography>{element.label}</Typography>
<Typography id={keyLabel}>{element.label}</Typography>
</Grid>
<Grid item lg={8} md={8} sm={8} xs={12}>
<InputText cid={keyCid} helpid={keyhelpid} type='text' value={value?.key?.char} controlProps={
<InputText id={keyCid} helpid={keyhelpid} type='text' value={value?.key?.char} controlProps={
{
onKeyDown: onKeyDown,
}
@@ -92,27 +95,27 @@ export default function KeyboardShortcuts({ value, onChange, fields }) {
</Grid>
</Grid>;
} else if (element.name == 'shift') {
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel}>
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel} key={_.uniqueId('c')}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={shiftCid} helpid={shifthelpid} value={value?.shift}
<InputCheckbox id={shiftCid} helpid={shifthelpid} value={value?.shift}
controlProps={ctrlProps}
onChange={onShiftChange} labelPlacement="end" ></InputCheckbox>
onChange={onShiftChange}></InputCheckbox>
</Box>
</Grid>;
} else if (element.name == 'control') {
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel}>
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel} key={_.uniqueId('c')}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={ctrlCid} helpid={ctrlhelpid} value={value?.ctrl}
<InputCheckbox id={ctrlCid} helpid={ctrlhelpid} value={value?.ctrl}
controlProps={ctrlProps}
onChange={onCtrlChange} labelPlacement="end" ></InputCheckbox>
onChange={onCtrlChange}></InputCheckbox>
</Box>
</Grid>;
} else if (element.name == 'alt') {
return <Grid item lg={3} md={3} sm={3} xs={12} className={classes.inputLabel}>
return <Grid item lg={3} md={3} sm={3} xs={12} className={classes.inputLabel} key={_.uniqueId('c')}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={altCid} helpid={althelpid} value={value?.alt}
<InputCheckbox id={altCid} helpid={althelpid} value={value?.alt}
controlProps={ctrlProps}
onChange={onAltChange} labelPlacement="end" ></InputCheckbox>
onChange={onAltChange}></InputCheckbox>
</Box>
</Grid>;
}

View File

@@ -298,7 +298,7 @@ export default function PgTable({ columns, data, isSelectRow, ...props }) {
className={classes.fixedSizeList}
height={height - 75}
itemCount={rows.length}
itemSize={35}
itemSize={35}
sorted={props?.sortOptions}
>
{RenderRow}

View File

@@ -61,7 +61,6 @@ export default function QueryThresholds({ value, onChange }) {
<Grid
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Grid item lg={2} md={2} sm={2} xs={12}>

View File

@@ -20,7 +20,7 @@ const useStyles = makeStyles(() => ({
}
}));
export default function Themes({onChange, ...props}) {
export default function SelectThemes({onChange, ...props}) {
const classes = useStyles();
const [previewSrc, setPreviewSrc] = useState(null);
@@ -48,7 +48,7 @@ export default function Themes({onChange, ...props}) {
);
}
Themes.propTypes = {
SelectThemes.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
controlProps: PropTypes.object,

View File

@@ -20,7 +20,7 @@ import Theme from '../Theme';
import HTMLReactParser from 'html-react-parser';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { Rnd } from 'react-rnd';
import { ExpandDialog, MinimizeDialog } from '../components/ExternalIcon';
import { ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon';
const ModalContext = React.createContext({});
@@ -154,6 +154,15 @@ function PaperComponent(props) {
let classes = dialogStyle();
let [dialogPosition, setDialogPosition] = useState(null);
let resizeable = props.isresizeable == 'true' ? true : false;
const setEnableResizing = () => {
return props.isfullscreen == 'true' ? false : resizeable;
};
const setConditionalPosition = () => {
return props.isfullscreen == 'true' ? { x: 0, y: 0 } : dialogPosition && { x: dialogPosition.x, y: dialogPosition.y };
};
return (
props.isresizeable == 'true' ?
<Rnd
@@ -168,8 +177,8 @@ function PaperComponent(props) {
{...(props.width && { minWidth: 500 })}
{...(props.width && { minHeight: 190 })}
bounds="window"
enableResizing={props.isfullscreen == 'true' ? false : resizeable}
position={props.isfullscreen == 'true' ? { x: 0, y: 0 } : dialogPosition && { x: dialogPosition.x, y: dialogPosition.y }}
enableResizing={setEnableResizing()}
position={setConditionalPosition()}
onDragStop={(e, position) => {
if (props.isfullscreen !== 'true') {
setDialogPosition({
@@ -241,11 +250,11 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, fullScr
<Box className={classes.title} marginRight="0.25rem" >{title}</Box>
{
showFullScreen && !isfullScreen &&
<Box marginLeft="auto"><PgIconButton title={gettext('Maximize')} icon={<ExpandDialog className={classes.icon} />} size="xs" noBorder onClick={() => { setIsFullScreen(!isfullScreen); }} /></Box>
<Box marginLeft="auto"><PgIconButton title={gettext('Maximize')} icon={<ExpandDialogIcon className={classes.icon} />} size="xs" noBorder onClick={() => { setIsFullScreen(!isfullScreen); }} /></Box>
}
{
showFullScreen && isfullScreen &&
<Box marginLeft="auto"><PgIconButton title={gettext('Minimize')} icon={<MinimizeDialog className={classes.icon} />} size="xs" noBorder onClick={() => { setIsFullScreen(!isfullScreen); }} /></Box>
<Box marginLeft="auto"><PgIconButton title={gettext('Minimize')} icon={<MinimizeDialogIcon className={classes.icon} />} size="xs" noBorder onClick={() => { setIsFullScreen(!isfullScreen); }} /></Box>
}
<Box marginLeft="auto"><PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={closeModal} /></Box>

View File

@@ -17,7 +17,7 @@ export class ManagePreferenceTreeNodes {
constructor(data) {
this.tree = {}
this.tempTree = new TreeNode(undefined, {});
this.treeData = data;
this.treeData = data || [];
}
public init = (_root: string) => new Promise((res, rej) => {
@@ -50,7 +50,7 @@ export class ManagePreferenceTreeNodes {
if (path === null || path === undefined || path.length === 0 || path == '/preferences') {
return this.tempTree;
}
console.log('Path', path)
return findInTree(this.tempTree, path);
}
@@ -109,13 +109,15 @@ export class ManagePreferenceTreeNodes {
await fill(self.treeData);
}
if (node?.children.length > 0) return res(node.children);
else return res(null);
self.returnChildrens(node, res)
}
loadData();
})
public returnChildrens = (node: any, res: any) =>{
if (node?.children.length > 0) return res(node.children);
else return res(null);
}
}

View File

@@ -1,53 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import * as React from 'react';
import { render } from 'react-dom';
import { FileTreeX, TreeModelX } from 'pgadmin4-tree';
import {Tree} from './tree';
import { IBasicFileSystemHost } from 'react-aspen';
import { ManagePreferenceTreeNodes } from './preference_nodes';
var initPreferencesTree = async (pgBrowser, container, data) => {
const MOUNT_POINT = '/preferences'
// Setup host
let ptree = new ManagePreferenceTreeNodes(data);
// Init Tree with the Tree Parent node '/browser'
ptree.init(MOUNT_POINT);
const host: IBasicFileSystemHost = {
pathStyle: 'unix',
getItems: async (path) => {
return ptree.readNode(path);
},
}
const pTreeModelX = new TreeModelX(host, MOUNT_POINT)
const itemHandle = function onReady(handler) {
// Initialize pgBrowser Tree
pgBrowser.ptree = new Tree(handler, ptree, pgBrowser, false);
return true;
}
await pTreeModelX.root.ensureLoaded()
// Render Browser Tree
await render(
<FileTreeX model={pTreeModelX}
onReady={itemHandle} />
, container);
}
module.exports = {
initPreferencesTree: initPreferencesTree,
};

View File

@@ -16,6 +16,7 @@ import {
OutlinedInput,
} from '@material-ui/core';
import KeyboardShortcuts from '../../../pgadmin/static/js/components/KeyboardShortcuts';
import { InputCheckbox } from '../../../pgadmin/static/js/components/FormComponents';
/* MUI Components need to be wrapped in Theme for theme vars */
describe('KeyboardShortcuts', () => {
@@ -26,7 +27,8 @@ describe('KeyboardShortcuts', () => {
'key': {
'char': 'a',
'key_code': 97
}
},
'shift': false
};
let fields = [{
type: 'keyCode',
@@ -63,21 +65,17 @@ describe('KeyboardShortcuts', () => {
describe('KeyboardShortcuts', () => {
let ThemedFormInputKeyboardShortcuts = withTheme(KeyboardShortcuts), ctrl;
let onChange = jasmine.createSpy('onChange');
beforeEach(() => {
ctrl = mount(
<ThemedFormInputKeyboardShortcuts
testcid="inpCid"
helpMessage="some help message"
/* InputText */
readonly={false}
disabled={false}
maxlength={1}
value={defult_value}
fields={fields}
controlProps={{
extraprop: 'test'
extraprop: 'test',
keyDown: onChange
}}
onChange={onChange}
/>);
});
@@ -85,15 +83,29 @@ describe('KeyboardShortcuts', () => {
expect(ctrl.find(OutlinedInput).prop('value')).toBe('a');
});
it('Key change', () => {
let onChange = () => {/*This is intentional (SonarQube)*/ };
ctrl.setProps({
controlProps: {
onKeyDown: onChange
}
});
it('Key change', (done) => {
ctrl.find(OutlinedInput).at(0).find('input').simulate('keydown', { key: '', keyCode: 32});
expect(onChange).toHaveBeenCalledWith({ ctrl: true, alt: true, key: { char: 'Space', key_code: 32 }, shift: false });
done();
});
expect(ctrl.find(OutlinedInput).prop('value')).toBe('a');
it('Shift option', (done) => {
ctrl.find(InputCheckbox).at(0).find('input').simulate('change', { target: { checked: true, name: 'shift' } });
expect(onChange).toHaveBeenCalledWith({ ctrl: true, alt: true, key: { char: 'a', key_code: 97 }, shift: true });
done();
});
it('Ctrl option', (done) => {
ctrl.find(InputCheckbox).at(1).find('input').simulate('change', { target: { checked: false, name: 'ctrl' } });
expect(onChange).toHaveBeenCalledWith({ ctrl: false, alt: true, key: { char: 'a', key_code: 97 }, shift: false });
done();
});
it('Alt option', (done) => {
ctrl.find(InputCheckbox).at(2).find('input').simulate('change', { target: { checked: false, name: 'alt' } });
expect(onChange).toHaveBeenCalledWith({ ctrl: true, alt: false, key: { char: 'a', key_code: 97 }, shift: false });
done();
});
});

View File

@@ -12,10 +12,8 @@ import React from 'react';
import '../helper/enzyme.helper';
import { withTheme } from '../fake_theme';
import { createMount } from '@material-ui/core/test-utils';
import {
OutlinedInput,
} from '@material-ui/core';
import QueryThresholds from '../../../pgadmin/static/js/components/QueryThresholds';
import { InputText } from '../../../pgadmin/static/js/components/FormComponents';
/* MUI Components need to be wrapped in Theme for theme vars */
describe('QueryThresholds', () => {
@@ -41,45 +39,34 @@ describe('QueryThresholds', () => {
describe('QueryThresholds', () => {
let ThemedFormInputQueryThresholds = withTheme(QueryThresholds), ctrl;
let onChange = jasmine.createSpy('onChange');
beforeEach(() => {
ctrl = mount(
<ThemedFormInputQueryThresholds
testcid="inpCid"
testcid="QueryThresholdCid"
helpMessage="some help message"
/* InputText */
readonly={false}
disabled={false}
maxlength={1}
value={defult_value}
controlProps={{
extraprop: 'test'
}}
onChange={onChange}
/>);
});
it('init Warning', () => {
expect(ctrl.find(OutlinedInput).at(0).prop('value')).toBe(5);
it('Warning', (done) => {
ctrl.find(InputText).at(0).find('input').simulate('change', { warning: 5, alert: 6 });
expect(ctrl.find(InputText).at(0).prop('value')).toBe(5);
expect(onChange).toHaveBeenCalledWith({ warning: '5', alert: 6 });
done();
});
it('init Alert', () => {
expect(ctrl.find(OutlinedInput).at(1).prop('value')).toBe(6);
});
it('Alert', (done) => {
ctrl.find(InputText).at(1).find('input').simulate('change', { warning: 5, alert: 6 });
expect(ctrl.find(InputText).at(1).prop('value')).toBe(6);
it('warning change', () => {
let onChange = () => {/*This is intentional (SonarQube)*/ };
ctrl.setProps({
onChange: onChange
});
expect(ctrl.find(OutlinedInput).at(0).prop('value')).toBe(5);
});
it('Alert change', () => {
let onChange = () => {/*This is intentional (SonarQube)*/ };
ctrl.setProps({
onChange: onChange
});
expect(ctrl.find(OutlinedInput).at(1).prop('value')).toBe(6);
expect(onChange).toHaveBeenCalledWith({ warning: 5, alert: '6' });
done();
});
});

View File

@@ -12,11 +12,11 @@ import React from 'react';
import '../helper/enzyme.helper';
import { withTheme } from '../fake_theme';
import { createMount } from '@material-ui/core/test-utils';
import Themes from '../../../pgadmin/static/js/components/Themes';
import SelectThemes from '../../../pgadmin/static/js/components/SelectThemes';
import { InputSelect } from '../../../pgadmin/static/js/components/FormComponents';
/* MUI Components need to be wrapped in Theme for theme vars */
describe('Themes', () => {
describe('SelectThemes', () => {
let mount;
let options = [{
value: 'standard',
@@ -51,13 +51,13 @@ describe('Themes', () => {
jasmineEnzyme();
});
describe('Themes', () => {
let ThemedFormInputThemes = withTheme(Themes), ctrl, onChange = jasmine.createSpy('onChange');
describe('Select Themes', () => {
let ThemedFormInputThemes = withTheme(SelectThemes), ctrl, onChange = jasmine.createSpy('onChange');
beforeEach(() => {
ctrl = mount(
<ThemedFormInputThemes
testcid="inpCid"
testcid="SelectThemeCid"
helpMessage="some help message"
options={options}
onChange={onChange}