Port preferences dialog to React. Fixes #7149

This commit is contained in:
Nikhil Mohite 2022-03-21 13:29:26 +05:30 committed by Akshay Joshi
parent 3299b0c1b0
commit 74e794b416
65 changed files with 2646 additions and 1006 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 115 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: 236 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -182,6 +182,18 @@ debugger window navigation:
:alt: Preferences dialog debugger keyboard shortcuts section
:align: center
The ERD Tool Node
*****************
Expand the *ERD Tool* node to specify your ERD Tool display preferences.
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
:align: center
The Miscellaneous Node
**********************

View File

@ -14,7 +14,7 @@ New features
Housekeeping
************
| `Issue #7149 <https://redmine.postgresql.org/issues/7149>`_ - Port preferences dialog to React.
Bug fixes
*********

View File

@ -8,8 +8,8 @@
"license": "PostgreSQL",
"devDependencies": {
"@babel/core": "^7.10.2",
"@babel/eslint-parser": "^7.12.13",
"@babel/eslint-plugin": "^7.12.13",
"@babel/eslint-parser": "^7.17.0",
"@babel/eslint-plugin": "^7.17.7",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1",
"@babel/plugin-syntax-jsx": "^7.16.0",
"@babel/preset-env": "^7.10.2",
@ -145,7 +145,7 @@
"path-fx": "^2.0.0",
"pathfinding": "^0.4.18",
"paths-js": "^0.4.9",
"pgadmin4-tree": "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422",
"pgadmin4-tree": "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613",
"postcss": "^8.2.15",
"raf": "^3.4.1",
"rc-dock": "^3.2.9",
@ -154,6 +154,7 @@
"react-checkbox-tree": "^1.7.2",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.4",
"react-rnd": "^10.3.5",
"react-select": "^4.2.1",
"react-table": "^7.6.3",
"react-timer-hook": "^3.0.5",

View File

@ -519,7 +519,7 @@ def register_browser_preferences(self):
self.open_in_new_tab = self.preference.register(
'tab_settings', 'new_browser_tab_open',
gettext("Open in new browser tab"), 'select2', None,
gettext("Open in new browser tab"), 'select', None,
category_label=PREF_LABEL_OPTIONS,
options=ope_new_tab_options,
help_str=gettext(
@ -527,7 +527,7 @@ def register_browser_preferences(self):
'or PSQL Tool from the drop-down to set '
'open in new browser tab for that particular module.'
),
select2={
control_props={
'multiple': True, 'allowClear': False,
'tags': True, 'first_empty': False,
'selectOnClose': False, 'emptyOptions': True,

View File

@ -0,0 +1,71 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
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 Notify from '../../../../../static/js/helpers/Notifier';
export function getBinaryPathSchema() {
return new BinaryPathSchema();
}
export default class BinaryPathSchema extends BaseUISchema {
constructor() {
super({
isDefault: false,
serverType: undefined,
binaryPath: null,
});
}
get baseFields() {
return [
{
id: 'isDefault', label: gettext('Set as default'), type: 'radio',
width: 32,
radioType: true,
disabled: function (state) {
return state?.binaryPath && state?.binaryPath.length > 0 ? false : true;
},
cell: 'radio',
deps: ['binaryPath'],
},
{
id: 'serverType',
label: gettext('Database Server'),
type: 'text', cell: '',
width: 40,
},
{
id: 'binaryPath', label: gettext('Binary Path'), cell: 'file', type: 'file',
isvalidate: true, controlProps: { dialogType: 'select_folder', supportedTypes: ['*', 'sql', 'backup'], dialogTitle: 'Select folder' },
validate: (data) => {
const api = getApiInstance();
if (_.isNull(data) || data.trim() === '') {
Notify.alert(gettext('Validate Path'), gettext('Path should not be empty.'));
} else {
api.post(url_for('misc.validate_binary_path'),
JSON.stringify({ 'utility_path': data }))
.then(function (res) {
Notify.alert(gettext('Validate binary path'), gettext(res.data.data));
})
.catch(function (error) {
Notify.pgNotifier(error, gettext('Failed to validate binary path.'));
});
}
return true;
}
},
];
}
}

View File

@ -67,7 +67,10 @@ class MiscModule(PgAdminModule):
'user_language', 'user_language',
gettext("User language"), 'options', 'en',
category_label=gettext('User language'),
options=lang_options
options=lang_options,
control_props={
'allowClear': False,
}
)
theme_options = []
@ -90,8 +93,11 @@ class MiscModule(PgAdminModule):
gettext("Theme"), 'options', 'standard',
category_label=gettext('Themes'),
options=theme_options,
control_props={
'allowClear': False,
},
help_str=gettext(
'A refresh is required to apply the theme. Below is the '
'A refresh is required to apply the theme. Above is the '
'preview of the theme'
)
)

View File

@ -167,10 +167,14 @@ class FileManagerModule(PgAdminModule):
)
self.file_dialog_view = self.preference.register(
'options', 'file_dialog_view',
gettext("File dialog view"), 'options', 'list',
gettext("File dialog view"), 'select', 'list',
category_label=PREF_LABEL_OPTIONS,
options=[{'label': gettext('List'), 'value': 'list'},
{'label': gettext('Grid'), 'value': 'grid'}]
{'label': gettext('Grid'), 'value': 'grid'}],
control_props={
'allowClear': False,
'tags': False
},
)
self.show_hidden_files = self.preference.register(
'options', 'show_hidden_files',
@ -236,7 +240,7 @@ def file_manager_config(trans_id):
"""render the required json"""
data = Filemanager.get_trasaction_selection(trans_id)
pref = Preferences.module('file_manager')
file_dialog_view = pref.preference('file_dialog_view').get()
file_dialog_view = pref.preference('file_dialog_view').get()[0]
show_hidden_files = pref.preference('show_hidden_files').get()
return Response(response=render_template(

View File

@ -37,26 +37,23 @@ class PreferencesModule(PgAdminModule):
"""
def get_own_javascripts(self):
return [{
'name': 'pgadmin.preferences',
'path': url_for('preferences.index') + 'preferences',
'when': None
}]
scripts = list()
for name, script in [
['pgadmin.preferences', 'js/preferences']
]:
scripts.append({
'name': name,
'path': url_for('preferences.index') + script,
'when': None
})
return scripts
def get_own_stylesheets(self):
return []
def get_own_menuitems(self):
return {
'file_items': [
MenuItem(name='mnu_preferences',
priority=997,
module="pgAdmin.Preferences",
callback='show',
icon='fa fa-cog',
label=gettext('Preferences'))
]
}
return {}
def get_exposed_url_endpoints(self):
"""
@ -149,7 +146,8 @@ def _iterate_categories(pref_d, label, res):
"label": gettext(pref_d['label']),
"inode": True,
"open": True,
"branch": []
"children": [],
"value": gettext(pref_d['label']),
}
for c in pref_d['categories']:
@ -162,13 +160,15 @@ def _iterate_categories(pref_d, label, res):
"id": c['id'],
"mid": pref_d['id'],
"label": gettext(c['label']),
"value": '{0}{1}'.format(c['id'], gettext(c['label'])),
"inode": False,
"open": False,
"preferences": sorted(c['preferences'], key=label)
"preferences": sorted(c['preferences'], key=label),
"showCheckbox": False
}
(om['branch']).append(oc)
om['branch'] = sorted(om['branch'], key=label)
(om['children']).append(oc)
om['children'] = sorted(om['children'], key=label)
res.append(om)
@ -194,53 +194,69 @@ def preferences_s():
)
@blueprint.route("/<int:pid>", methods=["PUT"], endpoint="update")
def get_data():
"""
Get preferences data.
:return: Preferences list
:rtype: list
"""
pref_data = request.form if request.form else json.loads(
request.data.decode())
if not pref_data:
raise ValueError("Please provide the valid preferences data to save.")
return pref_data
@blueprint.route("/", methods=["PUT"], endpoint="update")
@login_required
def save(pid):
def save():
"""
Save a specific preference.
"""
data = request.form if request.form else json.loads(request.data.decode())
pref_data = get_data()
if data['name'] in ['vw_edt_tab_title_placeholder',
'qt_tab_title_placeholder',
'debugger_tab_title_placeholder'] \
and data['value'].isspace():
data['value'] = ''
for data in pref_data:
if data['name'] in ['vw_edt_tab_title_placeholder',
'qt_tab_title_placeholder',
'debugger_tab_title_placeholder'] \
and data['value'].isspace():
data['value'] = ''
res, msg = Preferences.save(
data['mid'], data['category_id'], data['id'], data['value'])
sgm.get_nodes(sgm)
res, msg = Preferences.save(
data['mid'], data['category_id'], data['id'], data['value'])
sgm.get_nodes(sgm)
if not res:
return internal_server_error(errormsg=msg)
if not res:
return internal_server_error(errormsg=msg)
response = success_return()
response = success_return()
# Set cookie & session for language settings.
# This will execute every time as could not find the better way to know
# that which preference is getting updated.
# Set cookie & session for language settings.
# This will execute every time as could not find the better way to know
# that which preference is getting updated.
misc_preference = Preferences.module('misc')
user_languages = misc_preference.preference(
'user_language'
)
misc_preference = Preferences.module('misc')
user_languages = misc_preference.preference(
'user_language'
)
language = 'en'
if user_languages:
language = user_languages.get() or language
language = 'en'
if user_languages:
language = user_languages.get() or language
domain = dict()
if config.COOKIE_DEFAULT_DOMAIN and\
config.COOKIE_DEFAULT_DOMAIN != 'localhost':
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
domain = dict()
if config.COOKIE_DEFAULT_DOMAIN and \
config.COOKIE_DEFAULT_DOMAIN != 'localhost':
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
setattr(session, 'PGADMIN_LANGUAGE', language)
response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH,
secure=config.SESSION_COOKIE_SECURE,
httponly=config.SESSION_COOKIE_HTTPONLY,
samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
setattr(session, 'PGADMIN_LANGUAGE', language)
response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH,
secure=config.SESSION_COOKIE_SECURE,
httponly=config.SESSION_COOKIE_HTTPONLY,
samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
return response

View File

@ -0,0 +1,599 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import React, { useEffect } from 'react';
import { FileType } from 'react-aspen';
import { Box } from '@material-ui/core';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import SchemaView from '../../../../static/js/SchemaView';
import getApiInstance from '../../../../static/js/api_instance';
import CloseSharpIcon from '@material-ui/icons/CloseSharp';
import HelpIcon from '@material-ui/icons/HelpRounded';
import SaveSharpIcon from '@material-ui/icons/SaveSharp';
import clsx from 'clsx';
import Notify from '../../../../static/js/helpers/Notifier';
import pgAdmin from 'sources/pgadmin';
import { DefaultButton, PgIconButton, PrimaryButton } from '../../../../static/js/components/Buttons';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import { getBinaryPathSchema } from '../../../../browser/server_groups/servers/static/js/binary_path.ui';
import { _set_dynamic_tab } from '../../../../tools/datagrid/static/js/show_query_tool';
class PreferencesSchema extends BaseUISchema {
constructor(initValues = {}, schemaFields = []) {
super({
...initValues
});
this.schemaFields = schemaFields;
this.category = '';
}
get idAttribute() {
return 'id';
}
setSelectedCategory(category) {
this.category = category;
}
get baseFields() {
return this.schemaFields;
}
}
const useStyles = makeStyles((theme) =>
({
root: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '100%',
backgroundColor: theme.palette.background.default,
overflow: 'hidden',
'&$disabled': {
color: '#ddd',
}
},
body: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
preferences: {
borderColor: theme.otherVars.borderColor,
display: 'flex',
flexGrow: 1,
height: '100%',
minHeight: 0,
overflow: 'hidden'
},
treeContainer: {
flexBasis: '25%',
alignItems: 'flex-start',
paddingLeft: '5px',
minHeight: 0,
flexGrow: 1
},
tree: {
height: '100%',
flexGrow: 1
},
preferencesContainer: {
flexBasis: '75%',
padding: '5px',
borderColor: theme.otherVars.borderColor + '!important',
borderLeft: '1px solid',
position: 'relative',
height: '100%',
paddingTop: '5px',
overflow: 'auto'
},
actionBtn: {
alignItems: 'flex-start',
},
buttonMargin: {
marginLeft: '0.5em'
},
footer: {
borderTop: '1px solid #dde0e6 !important',
padding: '0.5rem',
display: 'flex',
width: '100%',
background: theme.otherVars.headerBg,
},
customTreeClass: {
'& .react-checkbox-tree': {
height: '100% !important',
border: 'none !important',
},
},
preferencesTree: {
height: 'calc(100% - 50px)',
minHeight: 0
}
}),
);
function RightPanel({ schema, ...props }) {
let initData = () => new Promise((resolve, reject) => {
try {
resolve(props.initValues);
} catch (error) {
reject(error);
}
});
return (
<SchemaView
formType={'dialog'}
getInitData={initData}
viewHelperProps={{ mode: 'edit' }}
schema={schema}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.onDataChange(changedData);
}}
/>
);
}
RightPanel.propTypes = {
schema: PropTypes.object,
initValues: PropTypes.object,
onDataChange: PropTypes.func
};
export default function PreferencesComponent({ ...props }) {
const classes = useStyles();
const [disableSave, setDisableSave] = React.useState(true);
const prefSchema = React.useRef(new PreferencesSchema({}, []));
const prefChangedData = React.useRef({});
const prefTreeInit = React.useRef(false);
const [prefTreeData, setPrefTreeData] = React.useState(null);
const [initValues, setInitValues] = React.useState({});
const [loadTree, setLoadTree] = React.useState(0);
const api = getApiInstance();
useEffect(() => {
const pref_url = url_for('preferences.index');
api({
url: pref_url,
method: 'GET',
}).then((res) => {
let preferencesData = [];
let preferencesTreeData = [];
let preferencesValues = {};
res.data.forEach(node => {
let id = Math.floor(Math.random() * 1000);
let tdata = {
'id': id.toString(),
'label': node.label,
'_label': node.label,
'name': node.label,
'icon': '',
'inode': true,
'type': 2,
'_type': node.label.toLowerCase(),
'_id': id,
'_pid': null,
'childrenNodes': [],
'expanded': true,
'isExpanded': true,
};
node.children.forEach(subNode => {
let sid = Math.floor(Math.random() * 1000);
let nodeData = {
'id': sid.toString(),
'label': subNode.label,
'_label': subNode.label,
'name': subNode.label,
'icon': '',
'inode': false,
'_type': subNode.label.toLowerCase(),
'_id': sid,
'_pid': node.id,
'type': 1,
'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'];
});
tdata['childrenNodes'].push(nodeData);
});
// set Preferences Tree data
preferencesTreeData.push(tdata);
});
setPrefTreeData(preferencesTreeData);
setInitValues(preferencesValues);
// set Preferences schema
prefSchema.current = new PreferencesSchema(preferencesValues, preferencesData);
}).catch((err) => {
Notify.alert(err);
});
}, []);
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) {
prefSchema.current.schemaFields.forEach((field) => {
field.visible = field.parentId === item._metadata.data.id;
});
setLoadTree(Math.floor(Math.random() * 1000));
initTreeTimeout = setTimeout(()=> {
prefTreeInit.current = true;
}, 10);
}
else {
if(item.isExpanded && item._children && item._children.length > 0 && prefTreeInit.current) {
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true);
}
}
});
// 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);
}
});
// 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) {
pgAdmin.Browser.ptree.tree.toggleDirectory(item);
}
});
/* Clear the initTreeTimeout timeout if unmounted */
return ()=>{
clearTimeout(initTreeTimeout);
};
}, [prefTreeData]);
function getControlMappedForType(type) {
switch (type) {
case 'text':
return 'text';
case 'input':
return 'text';
case 'boolean':
return 'switch';
case 'node':
return 'switch';
case 'integer':
return 'numeric';
case 'numeric':
return 'numeric';
case 'date':
return 'datetimepicker';
case 'datetime':
return 'datetimepicker';
case 'options':
return 'select';
case 'select':
return 'select';
case 'select2':
return 'select';
case 'multiline':
return 'multiline';
case 'switch':
return 'switch';
case 'keyboardshortcut':
return 'keyboardShortcut';
case 'radioModern':
return 'toggle';
case 'selectFile':
return 'file';
case 'threshold':
return 'threshold';
default:
if (console && console.warn) {
// Warning for developer only.
console.warn(
'Hmm.. We don\'t know how to render this type - \'\'' + type + '\' of control.'
);
}
return 'input';
}
}
function getCollectionValue(_metadata, value, initValues) {
let val = value;
if (typeof (value) == 'object') {
if (_metadata[0].type == 'collection' && _metadata[0].schema) {
if ('binaryPath' in value.changed[0]) {
let pathData = [];
let pathVersions = [];
value.changed.forEach((chValue) => {
pathVersions.push(chValue.version);
});
initValues[_metadata[0].id].forEach((initVal) => {
if (pathVersions.includes(initVal.version)) {
pathData.push(value.changed[pathVersions.indexOf(initVal.version)]);
}
else {
pathData.push(initVal);
}
});
val = JSON.stringify(pathData);
} else {
let key_val = {
'char': value.changed[0]['key'],
'key_code': value.changed[0]['code'],
};
value.changed[0]['key'] = key_val;
val = value.changed[0];
}
} else if ('warning' in value) {
val = value['warning'] + '|' + value['alert'];
} else if (value?.changed && value.changed.length > 0) {
val = JSON.stringify(value.changed);
}
}
return val;
}
function savePreferences(data, initValues) {
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);
_data.push({
'category_id': _metadata[0]['cid'],
'id': parseInt(key),
'mid': _metadata[0]['mid'],
'name': _metadata[0]['name'],
'value': val,
});
}
}
if (_data.length > 0) {
save(_data, data);
}
}
function checkRefreshRequired(pref, requires_refresh) {
if (pref.name == 'theme') {
requires_refresh = true;
}
if (pref.name == 'user_language') {
requires_refresh = true;
}
return requires_refresh;
}
function save(save_data, data) {
api({
url: url_for('preferences.index'),
method: 'PUT',
data: save_data,
}).then(() => {
let requires_refresh = false;
/* Find the modules changed */
let modulesChanged = {};
for (const [key] of Object.entries(data.current)) {
let pref = pgAdmin.Browser.get_preference_for_id(Number(key));
if (pref['name'] == 'dynamic_tabs') {
_set_dynamic_tab(pgAdmin.Browser, !pref['value']);
}
if (!modulesChanged[pref.module]) {
modulesChanged[pref.module] = true;
}
requires_refresh = checkRefreshRequired(pref, requires_refresh);
if (pref.name == 'hide_shared_server') {
Notify.confirm(
gettext('Browser tree refresh required'),
gettext('A browser tree refresh is required. Do you wish to refresh the tree?'),
function () {
pgAdmin.Browser.tree.destroy({
success: function () {
pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser);
return true;
},
});
},
function () {
return true;
},
gettext('Refresh'),
gettext('Later')
);
}
}
if (requires_refresh) {
Notify.confirm(
gettext('Refresh required'),
gettext('A page refresh is required to apply the theme. Do you wish to refresh the page now?'),
function () {
/* If user clicks Yes */
location.reload();
return true;
},
function () { props.closeModal(); /*props.panel.close()*/ },
gettext('Refresh'),
gettext('Later')
);
}
// Refresh preferences cache
pgAdmin.Browser.cache_preferences(modulesChanged);
props.closeModal(); /*props.panel.close()*/
}).catch((err) => {
Notify.alert(err.response.data);
});
}
const onDialogHelp = () => {
window.open(url_for('help.static', { 'filename': 'preferences.html' }), 'pgadmin_help');
};
return (
<Box height={'100%'}>
<Box className={classes.root}>
<Box className={clsx(classes.preferences)}>
<Box className={clsx(classes.treeContainer)} >
<Box className={clsx('aciTree', classes.tree)} id={'treeContainer'}></Box>
</Box>
<Box className={clsx(classes.preferencesContainer)}>
{
prefSchema.current && loadTree > 0 ?
<RightPanel schema={prefSchema.current} initValues={initValues} onDataChange={(changedData) => {
Object.keys(changedData).length > 0 ? setDisableSave(false) : setDisableSave(true);
prefChangedData.current = changedData;
}}></RightPanel>
: <></>
}
</Box>
</Box>
<Box className={classes.footer}>
<Box>
<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()*/ }} />}>
{gettext('Cancel')}
</DefaultButton>
<PrimaryButton className={classes.buttonMargin} startIcon={<SaveSharpIcon />} disabled={disableSave} onClick={() => { savePreferences(prefChangedData, initValues); }}>
{gettext('Save')}
</PrimaryButton>
</Box>
</Box>
{/* </Box> */}
</Box >
</Box>
);
}
PreferencesComponent.propTypes = {
schema: PropTypes.array,
initValues: PropTypes.object,
closeModal: PropTypes.func,
renderTree: PropTypes.func
};

View File

@ -0,0 +1,68 @@
// /////////////////////////////////////////////////////////////
// //
// // pgAdmin 4 - PostgreSQL Tools
// //
// // Copyright (C) 2013 - 2022, The pgAdmin Development Team
// // This software is released under the PostgreSQL Licence
// //
// //////////////////////////////////////////////////////////////
import * as React from 'react';
import { render } from 'react-dom';
import { Directory } from 'react-aspen';
import { FileTreeX, TreeModelX } from 'pgadmin4-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) => {
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 = {
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 pTreeModelX = new TreeModelX(host, MOUNT_POINT);
const itemHandle = function onReady(handler) {
// Initialize preferences Tree
pgBrowser.ptree = new Tree(handler, ptree, pgBrowser, 'preferences');
return true;
};
await pTreeModelX.root.ensureLoaded();
// Render Browser Tree
await render(
<FileTreeX model={pTreeModelX} height={'100%'}
onReady={itemHandle} />
, containerElement);
};
module.exports = {
initPreferencesTree: initPreferencesTree,
};

View File

@ -0,0 +1,21 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import Preferences from './preferences';
if(!pgAdmin.Preferences) {
pgAdmin.Preferences = {};
}
pgAdmin.Preferences = Preferences.getInstance(pgAdmin, pgBrowser);
module.exports = {
Preferences: Preferences,
};

View File

@ -7,624 +7,55 @@
//
//////////////////////////////////////////////////////////////
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';
define('pgadmin.preferences', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.backform',
'pgadmin.browser', 'sources/modify_animation',
'tools/datagrid/static/js/show_query_tool',
'sources/tree/pgadmin_tree_save_state',
], function(
gettext, url_for, $, _, Backbone, Alertify, pgAdmin, Backform, pgBrowser,
modifyAnimation, showQueryTool
) {
// This defines the Preference/Options Dialog for pgAdmin IV.
export default class Preferences {
static instance;
/*
* Hmm... this module is already been initialized, we can refer to the old
* object from here.
*/
if (pgAdmin.Preferences)
return pgAdmin.Preferences;
static getInstance(...args) {
if (!Preferences.instance) {
Preferences.instance = new Preferences(...args);
}
return Preferences.instance;
}
pgAdmin.Preferences = {
init: function() {
if (this.initialized)
return;
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
}
this.initialized = true;
init() {
if (this.initialized)
return;
this.initialized = true;
// Add Preferences in to file menu
var menus = [{
name: 'mnu_preferences',
module: this,
applies: ['file'],
callback: 'show',
enable: true,
priority: 3,
label: gettext('Preferences'),
icon: 'fa fa-cog',
}];
// Declare the Preferences dialog
Alertify.dialog('preferencesDlg', function() {
this.pgBrowser.add_menus(menus);
}
var jTree, // Variable to create the aci-tree
controls = [], // Keep tracking of all the backform controls
// created by the dialog.
// Dialog containter
$container = $('<div class=\'preferences_dialog d-flex flex-row\'></div>');
/*
* Preference Model
*
* This model will be used to keep tracking of the changes done for
* an individual option.
*/
var PreferenceModel = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
id: undefined,
value: undefined,
},
});
/*
* Preferences Collection object.
*
* We will use only one collection object to keep track of all the
* preferences.
*/
var changed = {},
preferences = this.preferences = new(Backbone.Collection.extend({
model: PreferenceModel,
url: url_for('preferences.index'),
updateAll: function() {
// We will send only the modified data to the server.
for (var key in changed) {
this.get(key).save();
}
return true;
},
}))(null);
preferences.on('reset', function() {
// Reset the changed variables
changed = {};
});
preferences.on('change', function(m) {
var id = m.get('id'),
dependents = m.get('dependents');
if (!(id in changed)) {
// Keep track of the original value
changed[id] = m._previousAttributes.value;
} else if (_.isEqual(m.get('value'), changed[id])) {
// Remove unchanged models.
delete changed[id];
}
// Check dependents exist or not. If exists then call dependentsFound function.
if (!_.isNull(dependents) && Array.isArray(dependents) && dependents.length > 0) {
dependentsFound(m.get('name'), m.get('value'), dependents);
}
});
/*
* Function: dependentsFound
*
* This method will be used to iterate through all the controls and
* dependents. If found then perform the appropriate action.
*/
var dependentsFound = function(pref_name, pref_val, dependents) {
// Iterate through all the controls and check the dependents
_.each(controls, function(c) {
let ctrl_name = c.model.get('name');
_.each(dependents, function(deps) {
if (ctrl_name === deps) {
// Create methods to take appropriate actions and call here.
enableDisableMaxWidth(pref_name, pref_val, c);
}
});
});
};
/*
* Function: enableDisableMaxWidth
*
* This method will be used to enable and disable Maximum Width control
*/
var enableDisableMaxWidth = function(pref_name, pref_val, control) {
if (pref_name === 'column_data_auto_resize' && pref_val === 'by_name') {
control.$el.find('input').prop('disabled', true);
control.$el.find('input').val(0);
} else if (pref_name === 'column_data_auto_resize' && pref_val === 'by_data') {
control.$el.find('input').prop('disabled', false);
}
};
/*
* Function: renderPreferencePanel
*
* Renders the preference panel in the content div based on the given
* preferences.
*/
var renderPreferencePanel = function(prefs) {
/*
* Clear the existing html in the preferences content
*/
var content = $container.find('.preferences_content');
/*
* We should clean up the existing controls.
*/
if (controls) {
_.each(controls, function(c) {
if ('$sel' in c) {
if (c.$sel.data('select2').isOpen()) c.$sel.data('select2').close();
}
c.remove();
});
}
content.empty();
controls = [];
/*
* We will create new set of controls and render it based on the
* list of preferences using the Backform Field, Control.
*/
_.each(prefs, function(p) {
var m = preferences.get(p.id);
m.errorModel = new Backbone.Model();
var f = new Backform.Field(
_.extend({}, p, {
id: 'value',
name: 'value',
})
),
cntr = new(f.get('control'))({
field: f,
model: m,
});
content.append(cntr.render().$el);
// We will keep track of all the controls rendered at the
// moment.
controls.push(cntr);
});
/* Iterate through all preferences and check if dependents found.
* If found then call the dependentsFound method
*/
_.each(prefs, function(p) {
let m = preferences.get(p.id);
let dependents = m.get('dependents');
if (!_.isNull(dependents) && Array.isArray(dependents) && dependents.length > 0) {
dependentsFound(m.get('name'), m.get('value'), dependents);
}
});
};
/*
* Function: dialogContentCleanup
*
* Do the dialog container cleanup on openning.
*/
var dialogContentCleanup = function() {
// Remove the existing preferences
if (!jTree)
return;
/*
* Remove the aci-tree (mainly to remove the jquery object of
* aciTree from the system for this container).
*/
try {
jTree.aciTree('destroy');
} catch (ex) {
// Sometimes - it fails to destroy the tree properly and throws
// exception.
console.warn(ex.stack || ex);
}
jTree.off('acitree', treeEventHandler);
// We need to reset the data from the preferences too
preferences.reset();
/*
* Clean up the existing controls.
*/
if (controls) {
_.each(controls, function(c) {
c.remove();
});
}
controls = [];
// Remove all the objects now.
$container.empty();
},
/*
* Function: selectFirstCategory
*
* Whenever a user select a module instead of a category, we should
* select the first categroy of it.
*/
selectFirstCategory = function(api, item) {
var data = item ? api.itemData(item) : null;
if (data && data.preferences) {
api.select(item);
return;
}
item = api.first(item);
selectFirstCategory(api, item);
},
/*
* A map on how to create controls for each datatype in preferences
* dialog.
*/
getControlMappedForType = function(p) {
switch (p.type) {
case 'text':
return 'input';
case 'boolean':
p.options = {
onText: gettext('True'),
offText: gettext('False'),
onColor: 'success',
offColor: 'ternary',
size: 'mini',
};
return 'switch';
case 'node':
p.options = {
onText: gettext('Show'),
offText: gettext('Hide'),
onColor: 'success',
offColor: 'ternary',
size: 'mini',
width: '56',
};
return 'switch';
case 'integer':
return 'numeric';
case 'numeric':
return 'numeric';
case 'date':
return 'datepicker';
case 'datetime':
return 'datetimepicker';
case 'options':
var opts = [],
has_value = false;
// Convert the array to SelectControl understandable options.
_.each(p.options, function(o) {
if ('label' in o && 'value' in o) {
let push_var = {
'label': o.label,
'value': o.value,
};
push_var['label'] = o.label;
push_var['value'] = o.value;
if('preview_src' in o) {
push_var['preview_src'] = o.preview_src;
}
opts.push(push_var);
if (o.value == p.value)
has_value = true;
} else {
opts.push({
'label': o,
'value': o,
});
if (o == p.value)
has_value = true;
}
});
if (p.select2 && p.select2.tags == true && p.value && has_value == false) {
opts.push({
'label': p.value,
'value': p.value,
});
}
p.options = opts;
return 'select2';
case 'select2':
var select_opts = [];
_.each(p.options, function(o) {
if ('label' in o && 'value' in o) {
let push_var = {
'label': o.label,
'value': o.value,
};
push_var['label'] = o.label;
push_var['value'] = o.value;
if('preview_src' in o) {
push_var['preview_src'] = o.preview_src;
}
select_opts.push(push_var);
} else {
select_opts.push({
'label': o,
'value': o,
});
}
});
p.options = select_opts;
return 'select2';
case 'multiline':
return 'textarea';
case 'switch':
return 'switch';
case 'keyboardshortcut':
return 'keyboardShortcut';
case 'radioModern':
return 'radioModern';
case 'selectFile':
return 'binary-paths-grid';
case 'threshold':
p.warning_label = gettext('Warning');
p.alert_label = gettext('Alert');
p.unit = gettext('(in minutes)');
return 'threshold';
default:
if (console && console.warn) {
// Warning for developer only.
console.warn(
'Hmm.. We don\'t know how to render this type - \'\'' + p.type + '\' of control.'
);
}
return 'input';
}
},
/*
* function: treeEventHandler
*
* It is basically a callback, which listens to aci-tree events,
* and act accordingly.
*
* + Selection of the node will existance of the preferences for
* the selected tree-node, if not pass on to select the first
* category under a module, else pass on to the render function.
*
* + When a new node is added in the tree, it will add the relavent
* preferences in the preferences model collection, which will be
* called during initialization itself.
*
*
*/
treeEventHandler = function(event, api, item, eventName) {
// Look for selected item (if none supplied)!
item = item || api.selected();
// Event tree item has itemData
var d = item ? api.itemData(item) : null;
/*
* boolean (switch/checkbox), string, enum (combobox - enumvals),
* integer (min-max), font, color
*/
switch (eventName) {
case 'selected':
if (!d)
break;
if (d.preferences) {
/*
* Clear the existing html in the preferences content
*/
$container.find('.preferences_content');
renderPreferencePanel(d.preferences);
break;
} else {
selectFirstCategory(api, item);
}
break;
case 'added':
if (!d)
break;
// We will add the preferences in to the preferences data
// collection.
if (d.preferences && _.isArray(d.preferences)) {
_.each(d.preferences, function(p) {
preferences.add({
'id': p.id,
'value': p.value,
'category_id': d.id,
'mid': d.mid,
'name': p.name,
'dependents': p.dependents,
});
/*
* We don't know until now, how to render the control for
* this preference.
*/
if (!p.control) {
p.control = getControlMappedForType(p);
}
if (p.help_str) {
p.helpMessage = p.help_str;
}
});
}
d.sortable = false;
break;
case 'loaded':
// Let's select the first category from the prefrences.
// We need to wait for sometime before all item gets loaded
// properly.
setTimeout(
function() {
selectFirstCategory(api, null);
}, 300);
break;
}
return true;
};
// Dialog property
return {
main: function() {
// Remove the existing content first.
dialogContentCleanup();
$container.append(
'<div class=\'pg-el-sm-3 preferences_tree aciTree\'></div>'
).append(
'<div class=\'pg-el-sm-9 preferences_content\'>' +
gettext('Category is not selected.') +
'</div>'
);
// Create the aci-tree for listing the modules and categories of
// it.
jTree = $container.find('.preferences_tree');
jTree.on('acitree', treeEventHandler);
jTree.aciTree({
selectable: true,
expand: true,
fullRow: true,
ajax: {
url: url_for('preferences.index'),
},
animateRoot: true,
unanimated: false,
show: {duration: 75},
hide: {duration: 75},
view: {duration: 75},
});
if (jTree.aciTree('api')) modifyAnimation.modifyAcitreeAnimation(pgBrowser, jTree.aciTree('api'));
this.show();
},
setup: function() {
return {
buttons: [{
text: '',
key: 112,
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs: {
name: 'dialog_help',
type: 'button',
label: gettext('Preferences'),
'aria-label': gettext('Help'),
url: url_for(
'help.static', {
'filename': 'preferences.html',
}
),
},
}, {
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
}, {
text: gettext('Save'),
key: 13,
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
}],
focus: {
element: 0,
},
options: {
padding: !1,
overflow: !1,
title: gettext('Preferences'),
closableByDimmer: false,
modal: true,
pinnable: false,
},
};
},
callback: function(e) {
if (e.button.element.name == 'dialog_help') {
e.cancel = true;
pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
null, null);
return;
}
if (e.button.text == gettext('Save')) {
let requires_refresh = false;
preferences.updateAll();
/* Find the modules changed */
let modulesChanged = {};
_.each(changed, (val, key)=> {
let pref = pgBrowser.get_preference_for_id(Number(key));
if(pref['name'] == 'dynamic_tabs') {
showQueryTool._set_dynamic_tab(pgBrowser, !pref['value']);
}
if(!modulesChanged[pref.module]) {
modulesChanged[pref.module] = true;
}
if(pref.name == 'theme') {
requires_refresh = true;
}
if(pref.name == 'hide_shared_server') {
Notify.confirm(
gettext('Browser tree refresh required'),
gettext('A browser tree refresh is required. Do you wish to refresh the tree?'),
function() {
pgAdmin.Browser.tree.destroy({
success: function() {
pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser);
return true;
},
});
},
function() {
preferences.reset();
changed = {};
return true;
},
gettext('Refresh'),
gettext('Later')
);
}
});
if(requires_refresh) {
Notify.confirm(
gettext('Refresh required'),
gettext('A page refresh is required to apply the theme. Do you wish to refresh the page now?'),
function() {
/* If user clicks Yes */
location.reload();
return true;
},
function() {/* If user clicks No */ return true;},
gettext('Refresh'),
gettext('Later')
);
}
// Refresh preferences cache
pgBrowser.cache_preferences(modulesChanged);
}
},
build: function() {
this.elements.content.appendChild($container.get(0));
Alertify.pgDialogBuild.apply(this);
},
hooks: {
onshow: function() {/* This is intentional (SonarQube) */},
},
};
});
},
show: function() {
Alertify.preferencesDlg(true).resizeTo(pgAdmin.Browser.stdW.calc(pgAdmin.Browser.stdW.lg),pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg));
},
};
return pgAdmin.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);
}} closeModal={closeModal} />;
}, { isFullScreen: false, isResizeable: true, showFullScreen: true, isFullWidth: true, dialogWidth: 900, dialogHeight: 550 });
}
}

View File

@ -0,0 +1,8 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

View File

@ -0,0 +1,27 @@
{
"get_preferences": [
{
"name": "Get the all Preferences",
"url": "/preferences/",
"is_positive_test": true,
"mocking_required": false,
"mock_data": {
},
"expected_data": {
"status_code": 200
}
}
],
"update_preferences": [
{
"name": "Update the Preferences",
"url": "/preferences/",
"is_positive_test": true,
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
]
}

View File

@ -0,0 +1,39 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import os
from pgadmin.utils.route import BaseTestGenerator
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
import json
import config
test_user_details = None
if config.SERVER_MODE:
test_user_details = config_data['pgAdmin4_test_non_admin_credentials']
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
with open(CURRENT_PATH + "/preferences_test_data.json") as data_file:
test_cases = json.load(data_file)
class GetPreferencesTest(BaseTestGenerator):
"""
This class will fetch all Preferences
"""
scenarios = utils.generate_scenarios('get_preferences', test_cases)
def runTest(self):
self.get_preferences()
def get_preferences(self):
response = self.tester.get(self.url,
content_type='html/json')
self.assertTrue(response.status_code, 200)

View File

@ -0,0 +1,60 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import os
from pgadmin.utils.route import BaseTestGenerator
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from regression import parent_node_dict
import json
import config
test_user_details = None
if config.SERVER_MODE:
test_user_details = config_data['pgAdmin4_test_non_admin_credentials']
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
with open(CURRENT_PATH + "/preferences_test_data.json") as data_file:
test_cases = json.load(data_file)
class GetPreferencesTest(BaseTestGenerator):
"""
This class will fetch all Preferences
"""
scenarios = utils.generate_scenarios('update_preferences', test_cases)
def setUp(self):
response = self.tester.get(self.url,
content_type='html/json')
self.assertTrue(response.status_code, 200)
parent_node_dict['preferences'] = response.data
def runTest(self):
self.update_preferences()
def update_preferences(self):
if 'preferences' in parent_node_dict:
data = \
json.loads(parent_node_dict['preferences'])[0]['children'][0][
'preferences'][0]
updated_data = [{
'id': data['id'],
'category_id': data['cid'],
'mid': data['mid'],
'name': data['name'],
'value': not data['value']
}]
response = self.tester.put(self.url,
data=json.dumps(updated_data),
content_type='html/json')
self.assertTrue(response.status_code, 200)
else:
self.fail('Preferences not found')

View File

@ -0,0 +1 @@
<svg width="512px" height="512px" viewBox="-32 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M212.686 315.314L120 408l32.922 31.029c15.12 15.12 4.412 40.971-16.97 40.971h-112C10.697 480 0 469.255 0 456V344c0-21.382 25.803-32.09 40.922-16.971L72 360l92.686-92.686c6.248-6.248 16.379-6.248 22.627 0l25.373 25.373c6.249 6.248 6.249 16.378 0 22.627zm22.628-118.628L328 104l-32.922-31.029C279.958 57.851 290.666 32 312.048 32h112C437.303 32 448 42.745 448 56v112c0 21.382-25.803 32.09-40.922 16.971L376 152l-92.686 92.686c-6.248 6.248-16.379 6.248-22.627 0l-25.373-25.373c-6.249-6.248-6.249-16.378 0-22.627z"/></svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -0,0 +1 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g data-name="Layer 2"><g data-name="collapse"><rect width="24" height="24" transform="rotate(180 12 12)" opacity="0"/><path d="M19 9h-2.58l3.29-3.29a1 1 0 1 0-1.42-1.42L15 7.57V5a1 1 0 0 0-1-1 1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 0-2z"/><path d="M10 13H5a1 1 0 0 0 0 2h2.57l-3.28 3.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L9 16.42V19a1 1 0 0 0 1 1 1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@ -359,7 +359,7 @@ export default function DataGridView({
/* Make sure to take the latest field info from schema */
field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field;
let {editable} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps);
let {editable, disabled} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps);
if(_.isUndefined(field.cell)) {
console.error('cell is required ', field);
@ -368,9 +368,17 @@ export default function DataGridView({
return <MappedCellControl rowIndex={row.index} value={value}
row={row.original} {...field}
readonly={!editable}
disabled={false}
disabled={disabled}
visible={true}
onCellChange={(changeValue)=>{
if(field.radioType) {
dataDispatch({
type: SCHEMA_STATE_ACTIONS.BULK_UPDATE,
path: accessPath,
value: changeValue,
id: field.id
});
}
dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat([row.index, field.id]),

View File

@ -9,18 +9,19 @@
import React, { useCallback } from 'react';
import _ from 'lodash';
import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, FormNote, FormInputDateTimePicker, PlainString, InputSQL,
InputSelect, InputText, InputCheckbox, InputDateTimePicker } from '../components/FormComponents';
import {
FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString,
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputThemes, InputRadio
} from '../components/FormComponents';
import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import { SelectRefresh} from '../components/SelectRefresh';
import { SelectRefresh } from '../components/SelectRefresh';
/* Control mapping for form view */
function MappedFormControlBase({type, value, id, onChange, className, visible, inputRef, noLabel, ...props}) {
function MappedFormControlBase({ type, value, id, onChange, className, visible, inputRef, noLabel, ...props }) {
const name = id;
const onTextChange = useCallback((e) => {
let val = e;
@ -34,36 +35,36 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
onChange && onChange(changedValue);
}, []);
if(!visible) {
if (!visible) {
return <></>;
}
/* The mapping uses Form* components as it comes with labels */
switch (type) {
case 'int':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='int'/>;
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='int' />;
case 'numeric':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='numeric'/>;
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='numeric' />;
case 'tel':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='tel'/>;
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='tel' />;
case 'text':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props}/>;
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />;
case 'multiline':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className}
inputRef={inputRef} controlProps={{multiline: true}} {...props}/>;
inputRef={inputRef} controlProps={{ multiline: true }} {...props} />;
case 'password':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} type='password' inputRef={inputRef} {...props}/>;
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} type='password' inputRef={inputRef} {...props} />;
case 'select':
return <FormInputSelect name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />;
case 'select-refresh':
return <SelectRefresh name={name} value={value} onChange={onTextChange} className={className} {...props} />;
case 'switch':
return <FormInputSwitch name={name} value={value}
onChange={(e)=>onTextChange(e.target.checked, e.target.name)} className={className}
onChange={(e) => onTextChange(e.target.checked, e.target.name)} className={className}
{...props} />;
case 'checkbox':
return <FormInputCheckbox name={name} value={value}
onChange={(e)=>onTextChange(e.target.checked, e.target.name)} className={className}
onChange={(e) => onTextChange(e.target.checked, e.target.name)} className={className}
{...props} />;
case 'toggle':
return <FormInputToggle name={name} value={value}
@ -76,9 +77,15 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
case 'sql':
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} {...props} />;
case 'note':
return <FormNote className={className} {...props}/>;
return <FormNote className={className} {...props} />;
case 'datetimepicker':
return <FormInputDateTimePicker name={name} value={value} onChange={onTextChange} className={className} {...props} />;
case 'keyboardShortcut':
return <FormInputKeyboardShortcut name={name} value={value} onChange={onTextChange} {...props}/>;
case 'threshold':
return <FormInputQueryThreshold name={name} value={value} onChange={onTextChange} {...props}/>;
case 'theme':
return <FormInputThemes name={name} value={value} onChange={onTextChange} {...props}/>;
default:
return <PlainString value={value} {...props} />;
}
@ -100,17 +107,25 @@ MappedFormControlBase.propTypes = {
};
/* Control mapping for grid cell view */
function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow,...props}) {
function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, ...props }) {
const name = id;
const onTextChange = useCallback((e) => {
let val = e;
if(e && e.target) {
if (e && e.target) {
val = e.target.value;
}
onCellChange && onCellChange(val);
}, []);
const onRadioChange = useCallback((e) => {
let val =e;
if(e && e.target) {
val = e.target.checked;
}
onCellChange && onCellChange(val);
});
const onSqlChange = useCallback((val) => {
onCellChange && onCellChange(val);
}, []);
@ -118,13 +133,13 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
/* Some grid cells are based on options selected in other cells.
* lets trigger a re-render for the row if optionsLoaded
*/
const optionsLoadedRerender = useCallback((res)=>{
const optionsLoadedRerender = useCallback((res) => {
/* optionsLoaded is called when select options are fetched */
optionsLoaded && optionsLoaded(res);
reRenderRow && reRenderRow();
}, []);
if(!visible) {
if (!visible) {
return <></>;
}
@ -152,6 +167,12 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
case 'sql':
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
case 'file':
return <InputFileSelect name={name} value={value} onChange={onTextChange} inputRef={props.inputRef} {...props} />;
case 'keyCode':
return <InputText name={name} value={value} onChange={onTextChange} {...props} type='text' maxlength={1} />;
case 'radio':
return <InputRadio name={name} value={value} onChange={onRadioChange} disabled={props.disabled} {...props} ></InputRadio>;
default:
return <PlainString value={value} {...props} />;
}
@ -167,14 +188,16 @@ MappedCellControlBase.propTypes = {
reRenderRow: PropTypes.func,
optionsLoaded: PropTypes.func,
onCellChange: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
disabled: PropTypes.bool,
inputRef: CustomPropTypes.ref,
};
const ALLOWED_PROPS_FIELD_COMMON = [
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
'orientation'
'orientation', 'isvalidate', 'fields', 'radioType'
];
const ALLOWED_PROPS_FIELD_FORM = [
@ -182,14 +205,14 @@ const ALLOWED_PROPS_FIELD_FORM = [
];
const ALLOWED_PROPS_FIELD_CELL = [
'cell', 'onCellChange', 'row', 'reRenderRow',
'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType'
];
export const MappedFormControl = (props)=>{
let newProps = {...props};
export const MappedFormControl = (props) => {
let newProps = { ...props };
let typeProps = evalFunc(null, newProps.type, newProps.state);
if(typeof(typeProps) === 'object') {
if (typeof (typeProps) === 'object') {
newProps = {
...newProps,
...typeProps,
@ -199,13 +222,13 @@ export const MappedFormControl = (props)=>{
}
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */
return <MappedFormControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM))}/>;
return <MappedFormControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM))} />;
};
export const MappedCellControl = (props)=>{
let newProps = {...props};
export const MappedCellControl = (props) => {
let newProps = { ...props };
let cellProps = evalFunc(null, newProps.cell, newProps.row);
if(typeof(cellProps) === 'object') {
if (typeof (cellProps) === 'object') {
newProps = {
...newProps,
...cellProps,
@ -215,5 +238,5 @@ export const MappedCellControl = (props)=>{
}
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */
return <MappedCellControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL))}/>;
return <MappedCellControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL))} />;
};

View File

@ -108,7 +108,7 @@ function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, i
/* The comparator and setter */
const attrChanged = (id, change, force=false)=>{
if(isValueEqual(_.get(origVal, id), _.get(sessVal, id)) && !force) {
if(isValueEqual(_.get(origVal, id), _.get(sessVal, id)) && !force && (_.isObject(_.get(origVal, id)) && _.isEqual(_.get(origVal, id), _.get(sessData, id)))) {
return;
} else {
change = change || _.get(sessVal, id);
@ -302,6 +302,7 @@ export const SCHEMA_STATE_ACTIONS = {
RERENDER: 'rerender',
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
DEFERRED_DEPCHANGE: 'deferred_depchange',
BULK_UPDATE: 'bulk_update'
};
const getDepChange = (currPath, newState, oldState, action)=>{
@ -354,6 +355,13 @@ const sessDataReducer = (state, action)=>{
case SCHEMA_STATE_ACTIONS.INIT:
data = action.payload;
break;
case SCHEMA_STATE_ACTIONS.BULK_UPDATE:
rows = (_.get(data, action.path)||[]);
rows.forEach((row)=> {
row[action.id] = false;
});
_.set(data, action.path, rows);
break;
case SCHEMA_STATE_ACTIONS.SET_VALUE:
_.set(data, action.path, action.value);
/* If there is any dep listeners get the changes */

View File

@ -11,9 +11,11 @@ import DisconnectedSvg from '../../img/fonticon/disconnected.svg?svgr';
import RegexSvg from '../../img/fonticon/regex.svg?svgr';
import FormatCaseSvg from '../../img/fonticon/format_case.svg?svgr';
import PropTypes from 'prop-types';
import Expand from '../../img/fonticon/expand.svg?svgr';
import Collapse from '../../img/fonticon/minimize_collapse.svg?svgr';
export default function ExternalIcon({Icon, ...props}) {
return <Icon className='MuiSvgIcon-root' {...props} />;
return <Icon className={'MuiSvgIcon-root'} {...props} />;
}
ExternalIcon.propTypes = {
@ -31,4 +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'}} />;

View File

@ -10,8 +10,10 @@
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Box, FormControl, OutlinedInput, FormHelperText,
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect } from '@material-ui/core';
import {
Box, FormControl, OutlinedInput, FormHelperText,
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect, Radio,
} from '@material-ui/core';
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
import ErrorRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
import InfoRoundedIcon from '@material-ui/icons/InfoRounded';
@ -20,13 +22,14 @@ import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import WarningRoundedIcon from '@material-ui/icons/WarningRounded';
import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded';
import DescriptionIcon from '@material-ui/icons/Description';
import Select, {components as RSComponents} from 'react-select';
import AssignmentTurnedIn from '@material-ui/icons/AssignmentTurnedIn';
import Select, { components as RSComponents } from 'react-select';
import CreatableSelect from 'react-select/creatable';
import Pickr from '@simonwep/pickr';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import HTMLReactParse from 'html-react-parser';
import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider} from '@material-ui/pickers';
import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
import DateFnsUtils from '@date-io/date-fns';
import * as DateFns from 'date-fns';
@ -36,6 +39,9 @@ import { showFileDialog } from '../helpers/legacyConnector';
import _ from 'lodash';
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
import CustomPropTypes from '../custom_prop_types';
import KeyboardShortcuts from './KeyboardShortcuts';
import QueryThresholds from './QueryThresholds';
import Themes from './Themes';
const useStyles = makeStyles((theme) => ({
@ -55,7 +61,7 @@ const useStyles = makeStyles((theme) => ({
margin: theme.spacing(0.75, 0.75, 0.75, 0.75),
display: 'flex',
},
formLabelError: {
formLabelError: {
color: theme.palette.error.main,
},
sql: {
@ -95,17 +101,17 @@ export const MESSAGE_TYPE = {
};
/* Icon based on MESSAGE_TYPE */
function FormIcon({type, close=false, ...props}) {
function FormIcon({ type, close = false, ...props }) {
let TheIcon = null;
if(close) {
if (close) {
TheIcon = CloseIcon;
} else if(type === MESSAGE_TYPE.SUCCESS) {
} else if (type === MESSAGE_TYPE.SUCCESS) {
TheIcon = CheckRoundedIcon;
} else if(type === MESSAGE_TYPE.ERROR) {
} else if (type === MESSAGE_TYPE.ERROR) {
TheIcon = ErrorRoundedIcon;
} else if(type === MESSAGE_TYPE.INFO) {
} else if (type === MESSAGE_TYPE.INFO) {
TheIcon = InfoRoundedIcon;
} else if(type === MESSAGE_TYPE.WARNING) {
} else if (type === MESSAGE_TYPE.WARNING) {
TheIcon = WarningRoundedIcon;
}
@ -117,21 +123,21 @@ FormIcon.propTypes = {
};
/* Wrapper on any form component to add label, error indicator and help message */
export function FormInput({children, error, className, label, helpMessage, required, testcid}) {
export function FormInput({ children, error, className, label, helpMessage, required, testcid }) {
const classes = useStyles();
const cid = testcid || _.uniqueId('c');
const helpid = `h${cid}`;
return (
<Grid container spacing={0} className={className}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error?classes.formLabelError : null)} required={required}>
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>
{label}
<FormIcon type={MESSAGE_TYPE.ERROR} style={{marginLeft: 'auto', visibility: error ? 'unset' : 'hidden'}}/>
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<FormControl error={Boolean(error)} fullWidth>
{React.cloneElement(children, {cid, helpid})}
{React.cloneElement(children, { cid, helpid })}
</FormControl>
<FormHelperText id={helpid} variant="outlined">{HTMLReactParse(helpMessage || '')}</FormHelperText>
</Grid>
@ -148,17 +154,22 @@ FormInput.propTypes = {
testcid: PropTypes.any,
};
export function InputSQL({value, onChange, className, controlProps, ...props}) {
export function InputSQL({ value, options, onChange, className, controlProps, ...props }) {
const classes = useStyles();
const editor = useRef();
return (
<CodeMirror
currEditor={(obj)=>editor.current=obj}
value={value||''}
currEditor={(obj) => editor.current = obj}
value={value || ''}
options={{
lineNumbers: true,
mode: 'text/x-pgsql',
...options,
}}
className={clsx(classes.sql, className)}
events={{
change: (cm)=>{
change: (cm) => {
onChange && onChange(cm.getValue());
},
}}
@ -176,13 +187,13 @@ InputSQL.propTypes = {
controlProps: PropTypes.object,
};
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, noLabel, ...props}) {
if(noLabel) {
return <InputSQL value={value} {...props}/>;
export function FormInputSQL({ hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props }) {
if (noLabel) {
return <InputSQL value={value} options={controlProps} {...props} />;
} else {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} {...props}/>
<InputSQL value={value} options={controlProps} {...props} />
</FormInput>
);
}
@ -208,7 +219,7 @@ const DATE_TIME_FORMAT = {
TIME_24: 'HH:mm:ss',
};
export function InputDateTimePicker({value, onChange, readonly, controlProps, ...props}) {
export function InputDateTimePicker({ value, onChange, readonly, controlProps, ...props }) {
let format = '';
let placeholder = '';
if (controlProps?.pickerType === 'Date') {
@ -222,15 +233,15 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
placeholder = controlProps.placeholder || 'YYYY-MM-DD HH:mm:ss Z';
}
const handleChange = (dateVal, stringVal)=> {
const handleChange = (dateVal, stringVal) => {
onChange(stringVal);
};
/* Value should be a date object instead of string */
value = _.isUndefined(value) ? null : value;
if(!_.isNull(value)) {
if (!_.isNull(value)) {
let parseValue = DateFns.parse(value, format, new Date());
if(!DateFns.isValid(parseValue)) {
if (!DateFns.isValid(parseValue)) {
parseValue = DateFns.parseISO(value);
}
value = !DateFns.isValid(parseValue) ? value : parseValue;
@ -238,7 +249,7 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
if (readonly) {
return (<InputText value={value ? DateFns.format(value, format) : value}
readonly={readonly} controlProps={{placeholder: controlProps.placeholder}} {...props}/>);
readonly={readonly} controlProps={{ placeholder: controlProps.placeholder }} {...props} />);
}
let commonProps = {
@ -262,20 +273,20 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
if (controlProps?.pickerType === 'Date') {
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker {...commonProps}/>
<KeyboardDatePicker {...commonProps} />
</MuiPickersUtilsProvider>
);
} else if (controlProps?.pickerType === 'Time') {
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardTimePicker {...commonProps}/>
<KeyboardTimePicker {...commonProps} />
</MuiPickersUtilsProvider>
);
}
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDateTimePicker {...commonProps}/>
<KeyboardDateTimePicker {...commonProps} />
</MuiPickersUtilsProvider>
);
}
@ -287,10 +298,10 @@ InputDateTimePicker.propTypes = {
controlProps: PropTypes.object,
};
export function FormInputDateTimePicker({hasError, required, label, className, helpMessage, testcid, ...props}) {
export function FormInputDateTimePicker({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputDateTimePicker {...props}/>
<InputDateTimePicker {...props} />
</FormInput>
);
}
@ -308,23 +319,23 @@ FormInputDateTimePicker.propTypes = {
/* Use forwardRef to pass ref prop to OutlinedInput */
export const InputText = forwardRef(({
cid, helpid, readonly, disabled, maxlength=255, value, onChange, controlProps, type, ...props}, ref)=>{
cid, helpid, readonly, disabled, maxlength = 255, value, onChange, controlProps, type, ...props }, ref) => {
const classes = useStyles();
const patterns = {
'numeric': '^-?[0-9]\\d*\\.?\\d*$',
'int': '^-?[0-9]\\d*$',
};
let onChangeFinal = (e)=>{
let onChangeFinal = (e) => {
let changeVal = e.target.value;
/* For type number, we set type as tel with number regex to get validity.*/
if(['numeric', 'int', 'tel'].indexOf(type) > -1) {
if(!e.target.validity.valid && changeVal !== '' && changeVal !== '-') {
if (['numeric', 'int', 'tel'].indexOf(type) > -1) {
if (!e.target.validity.valid && changeVal !== '' && changeVal !== '-') {
return;
}
}
if(controlProps?.formatter) {
if (controlProps?.formatter) {
changeVal = controlProps.formatter.toRaw(changeVal);
}
onChange && onChange(changeVal);
@ -332,11 +343,11 @@ export const InputText = forwardRef(({
let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;
if(controlProps?.formatter) {
if (controlProps?.formatter) {
finalValue = controlProps.formatter.fromRaw(finalValue);
}
return(
return (
<OutlinedInput
ref={ref}
color="primary"
@ -346,7 +357,7 @@ export const InputText = forwardRef(({
id: cid,
maxLength: controlProps?.multiline ? null : maxlength,
'aria-describedby': helpid,
...(type ? {pattern: !_.isUndefined(controlProps) && !_.isUndefined(controlProps.pattern) ? controlProps.pattern : patterns[type]} : {})
...(type ? { pattern: !_.isUndefined(controlProps) && !_.isUndefined(controlProps.pattern) ? controlProps.pattern : patterns[type] } : {})
}}
readOnly={Boolean(readonly)}
disabled={Boolean(disabled)}
@ -354,9 +365,12 @@ export const InputText = forwardRef(({
notched={false}
value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue}
onChange={onChangeFinal}
{
...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown })
}
{...controlProps}
{...props}
{...(['numeric', 'int'].indexOf(type) > -1 ? {type: 'tel'} : {type: type})}
{...(['numeric', 'int'].indexOf(type) > -1 ? { type: 'tel' } : { type: type })}
/>
);
});
@ -374,10 +388,10 @@ InputText.propTypes = {
type: PropTypes.string,
};
export function FormInputText({hasError, required, label, className, helpMessage, testcid, ...props}) {
export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputText label={label} {...props}/>
<InputText label={label} {...props} />
</FormInput>
);
}
@ -391,16 +405,21 @@ FormInputText.propTypes = {
};
/* Using the existing file dialog functions using showFileDialog */
export function InputFileSelect({controlProps, onChange, disabled, readonly, ...props}) {
export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, validate, ...props }) {
const inpRef = useRef();
const onFileSelect = (value)=>{
const onFileSelect = (value) => {
onChange && onChange(decodeURI(value));
inpRef.current.focus();
};
return (
<InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} {...props} endAdornment={
<IconButton onClick={()=>showFileDialog(controlProps, onFileSelect)}
disabled={disabled||readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
<>
<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>
}
</>
} />
);
}
@ -409,14 +428,17 @@ InputFileSelect.propTypes = {
onChange: PropTypes.func,
disabled: PropTypes.bool,
readonly: PropTypes.bool,
isvalidate: PropTypes.bool,
validate: PropTypes.func,
value: PropTypes.string
};
export function FormInputFileSelect({
hasError, required, label, className, helpMessage, testcid, ...props}) {
hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputFileSelect required={required} label={label} {...props}/>
<InputFileSelect required={required} label={label} {...props} />
</FormInput>
);
}
@ -429,13 +451,13 @@ FormInputFileSelect.propTypes = {
testcid: PropTypes.string,
};
export function InputSwitch({cid, helpid, value, onChange, readonly, controlProps, ...props}) {
export function InputSwitch({ cid, helpid, value, onChange, readonly, controlProps, ...props }) {
const classes = useStyles();
return (
<Switch color="primary"
checked={Boolean(value)}
onChange={
readonly ? ()=>{/*This is intentional (SonarQube)*/} : onChange
readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange
}
id={cid}
inputProps={{
@ -457,11 +479,11 @@ InputSwitch.propTypes = {
controlProps: PropTypes.object,
};
export function FormInputSwitch({hasError, required, label, className, helpMessage, testcid, ...props}) {
export function FormInputSwitch({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputSwitch {...props}/>
<InputSwitch {...props} />
</FormInput>
);
}
@ -474,7 +496,7 @@ FormInputSwitch.propTypes = {
testcid: PropTypes.string,
};
export function InputCheckbox({cid, helpid, value, onChange, controlProps, readonly, ...props}) {
export function InputCheckbox({ cid, helpid, value, onChange, controlProps, readonly, ...props }) {
controlProps = controlProps || {};
return (
<FormControlLabel
@ -482,12 +504,13 @@ export function InputCheckbox({cid, helpid, value, onChange, controlProps, reado
<Checkbox
id={cid}
checked={Boolean(value)}
onChange={readonly ? ()=>{/*This is intentional (SonarQube)*/} : onChange}
onChange={readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange}
color="primary"
inputProps={{'aria-describedby': helpid}}
{...props}/>
inputProps={{ 'aria-describedby': helpid }}
{...props} />
}
label={controlProps.label}
labelPlacement={props?.labelPlacement ? props.labelPlacement : 'end'}
/>
);
}
@ -498,14 +521,15 @@ InputCheckbox.propTypes = {
controlProps: PropTypes.object,
onChange: PropTypes.func,
readonly: PropTypes.bool,
labelPlacement: PropTypes.string
};
export function FormInputCheckbox({hasError, required, label,
className, helpMessage, testcid, ...props}) {
export function FormInputCheckbox({ hasError, required, label,
className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputCheckbox {...props}/>
<InputCheckbox {...props} />
</FormInput>
);
}
@ -518,24 +542,61 @@ FormInputCheckbox.propTypes = {
testcid: PropTypes.string,
};
export function InputRadio({ helpid, value, onChange, controlProps, readonly, ...props }) {
const classes = useStyles();
controlProps = controlProps || {};
return (
<FormControlLabel
control={
<Radio
color="primary"
checked={props?.disabled ? false : value }
onChange={
readonly ? () => {
/*This is intentional (SonarQube)*/ } : onChange
}
value={value}
name="radio-button-demo"
inputProps={{ 'aria-label': value, 'aria-describedby': helpid }}
style={{ padding: 0 }}
disableRipple
{...props}
/>
}
label={controlProps.label}
className={(readonly || props.disabled) ? classes.readOnlySwitch : null}
/>
);
}
InputRadio.propTypes = {
helpid: PropTypes.string,
value: PropTypes.bool,
controlProps: PropTypes.object,
onChange: PropTypes.func,
readonly: PropTypes.bool,
disabled: PropTypes.bool,
labelPlacement: PropTypes.string
};
export const InputToggle = forwardRef(({cid, value, onChange, options, disabled, readonly, ...props}, ref) => {
export const InputToggle = forwardRef(({ cid, value, onChange, options, disabled, readonly, ...props }, ref) => {
return (
<ToggleButtonGroup
id={cid}
value={value}
exclusive
onChange={(e, val)=>{val!==null && onChange(val);}}
onChange={(e, val) => { val !== null && onChange(val); }}
{...props}
>
{
(options||[]).map((option, i)=>{
(options || []).map((option, i) => {
const isSelected = option.value === value;
const isDisabled = disabled || option.disabled || (readonly && !isSelected);
return (
<ToggleButton ref={i==0 ? ref : null} key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}
<ToggleButton ref={i == 0 ? ref : null} key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}
disabled={isDisabled} aria-label={option.label}>
<CheckRoundedIcon style={{visibility: isSelected ? 'visible': 'hidden'}}/>&nbsp;{option.label}
<CheckRoundedIcon style={{ visibility: isSelected ? 'visible' : 'hidden' }} />&nbsp;{option.label}
</ToggleButton>
);
})
@ -554,11 +615,11 @@ InputToggle.propTypes = {
readonly: PropTypes.bool,
};
export function FormInputToggle({hasError, required, label,
className, helpMessage, testcid, inputRef, ...props}) {
export function FormInputToggle({ hasError, required, label,
className, helpMessage, testcid, inputRef, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputToggle ref={inputRef} {...props}/>
<InputToggle ref={inputRef} {...props} />
</FormInput>
);
}
@ -575,9 +636,9 @@ FormInputToggle.propTypes = {
/* react-select package is used for select input
* Customizing the select styles to fit existing theme
*/
const customReactSelectStyles = (theme, readonly)=>({
const customReactSelectStyles = (theme, readonly) => ({
input: (provided) => {
return {...provided, padding: 0, margin: 0, color: 'inherit'};
return { ...provided, padding: 0, margin: 0, color: 'inherit' };
},
singleValue: (provided) => {
return {
@ -593,35 +654,35 @@ const customReactSelectStyles = (theme, readonly)=>({
borderColor: theme.otherVars.inputBorderColor,
...(state.isFocused ? {
borderColor: theme.palette.primary.main,
boxShadow: 'inset 0 0 0 1px '+theme.palette.primary.main,
boxShadow: 'inset 0 0 0 1px ' + theme.palette.primary.main,
'&:hover': {
borderColor: theme.palette.primary.main,
}
} : {}),
}),
dropdownIndicator: (provided)=>({
dropdownIndicator: (provided) => ({
...provided,
padding: '0rem 0.25rem',
}),
indicatorsContainer: (provided)=>({
indicatorsContainer: (provided) => ({
...provided,
...(readonly ? {display: 'none'} : {})
...(readonly ? { display: 'none' } : {})
}),
clearIndicator: (provided)=>({
clearIndicator: (provided) => ({
...provided,
padding: '0rem 0.25rem',
}),
valueContainer: (provided)=>({
valueContainer: (provided) => ({
...provided,
padding: theme.otherVars.reactSelect.padding,
}),
groupHeading: (provided)=>({
groupHeading: (provided) => ({
...provided,
color: 'inherit',
fontSize: '0.85em',
textTransform: 'none',
}),
menu: (provided)=>({
menu: (provided) => ({
...provided,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
@ -629,12 +690,12 @@ const customReactSelectStyles = (theme, readonly)=>({
border: '1px solid ' + theme.otherVars.inputBorderColor,
marginTop: '2px',
}),
menuPortal: (provided)=>({
menuPortal: (provided) => ({
...provided, zIndex: 9999,
backgroundColor: 'inherit',
color: 'inherit',
}),
option: (provided, state)=>{
option: (provided, state) => {
let bgColor = 'inherit';
if (state.isFocused) {
bgColor = theme.palette.grey[400];
@ -648,27 +709,27 @@ const customReactSelectStyles = (theme, readonly)=>({
backgroundColor: bgColor,
};
},
multiValue: (provided)=>({
multiValue: (provided) => ({
...provided,
backgroundColor: theme.palette.grey[400],
}),
multiValueLabel: (provided)=>({
multiValueLabel: (provided) => ({
...provided,
fontSize: '1em',
zIndex: 99,
color: theme.palette.text.primary
}),
multiValueRemove: (provided)=>({
multiValueRemove: (provided) => ({
...provided,
'&:hover': {
backgroundColor: 'unset',
color: theme.palette.error.main,
},
...(readonly ? {display: 'none'} : {})
...(readonly ? { display: 'none' } : {})
}),
});
function OptionView({image, label}) {
function OptionView({ image, label }) {
const classes = useStyles();
return (
<>
@ -705,8 +766,8 @@ CustomSelectSingleValue.propTypes = {
};
export function flattenSelectOptions(options) {
return _.flatMap(options, (option)=>{
if(option.options) {
return _.flatMap(options, (option) => {
if (option.options) {
return option.options;
} else {
return option;
@ -716,28 +777,28 @@ export function flattenSelectOptions(options) {
function getRealValue(options, value, creatable, formatter) {
let realValue = null;
if(_.isArray(value)) {
if (_.isArray(value)) {
realValue = [...value];
/* If multi select options need to be in some format by UI, use formatter */
if(formatter) {
if (formatter) {
realValue = formatter.fromRaw(realValue, options);
} else {
if(creatable) {
realValue = realValue.map((val)=>({label:val, value: val}));
if (creatable) {
realValue = realValue.map((val) => ({ label: val, value: val }));
} else {
realValue = realValue.map((val)=>(_.find(options, (option)=>_.isEqual(option.value, val))));
realValue = realValue.map((val) => (_.find(options, (option) => _.isEqual(option.value, val))));
}
}
} else {
let flatOptions = flattenSelectOptions(options);
realValue = _.find(flatOptions, (option)=>option.value==value) ||
(creatable && !_.isUndefined(value) && !_.isNull(value) ? {label:value, value: value} : null);
realValue = _.find(flatOptions, (option) => option.value == value) ||
(creatable && !_.isUndefined(value) && !_.isNull(value) ? { label: value, value: value } : null);
}
return realValue;
}
export function InputSelectNonSearch({options, ...props}) {
export function InputSelectNonSearch({ options, ...props }) {
return <MuiSelect native {...props} variant="outlined">
{(options||[]).map((o)=><option key={o.value} value={o.value}>{o.label}</option>)}
{(options || []).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</MuiSelect>;
}
InputSelectNonSearch.propTypes = {
@ -748,7 +809,7 @@ InputSelectNonSearch.propTypes = {
};
export const InputSelect = forwardRef(({
cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, optionsReloadBasis, disabled, ...props}, ref) => {
cid, onChange, options, readonly = false, value, controlProps = {}, optionsLoaded, optionsReloadBasis, disabled, ...props }, ref) => {
const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
const theme = useTheme();
@ -757,33 +818,33 @@ export const InputSelect = forwardRef(({
loading the options. optionsReloadBasis is helpful to avoid repeated
options load. If optionsReloadBasis value changes, then options will be loaded again.
*/
useEffect(()=>{
let optPromise = options, umounted=false;
if(typeof options === 'function') {
useEffect(() => {
let optPromise = options, umounted = false;
if (typeof options === 'function') {
optPromise = options();
}
setFinalOptions([[], true]);
Promise.resolve(optPromise)
.then((res)=>{
.then((res) => {
/* If component unmounted, dont update state */
if(!umounted) {
if (!umounted) {
optionsLoaded && optionsLoaded(res, value);
/* Auto select if any option has key as selected */
const flatRes = flattenSelectOptions(res || []);
let selectedVal;
if(controlProps.multiple) {
selectedVal = _.filter(flatRes, (o)=>o.selected)?.map((o)=>o.value);
if (controlProps.multiple) {
selectedVal = _.filter(flatRes, (o) => o.selected)?.map((o) => o.value);
} else {
selectedVal = _.find(flatRes, (o)=>o.selected)?.value;
selectedVal = _.find(flatRes, (o) => o.selected)?.value;
}
if((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) {
if ((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) {
onChange && onChange(selectedVal);
}
setFinalOptions([res || [], false]);
}
});
return ()=>umounted=true;
return () => umounted = true;
}, [optionsReloadBasis]);
@ -791,7 +852,7 @@ export const InputSelect = forwardRef(({
const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions;
const flatFiltered = flattenSelectOptions(filteredOptions);
let realValue = getRealValue(flatFiltered, value, controlProps.creatable, controlProps.formatter);
if(realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) {
if (realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) {
console.error('Undefined option value not allowed', realValue, filteredOptions);
}
const otherProps = {
@ -802,17 +863,17 @@ export const InputSelect = forwardRef(({
const styles = customReactSelectStyles(theme, readonly || disabled);
const onChangeOption = useCallback((selectVal)=>{
if(_.isArray(selectVal)) {
const onChangeOption = useCallback((selectVal) => {
if (_.isArray(selectVal)) {
// Check if select all option is selected
if (!_.isUndefined(selectVal.find(x => x.label === 'Select All'))) {
selectVal = filteredOptions;
}
/* If multi select options need to be in some format by UI, use formatter */
if(controlProps.formatter) {
if (controlProps.formatter) {
selectVal = controlProps.formatter.toRaw(selectVal, filteredOptions);
} else {
selectVal = selectVal.map((option)=>option.value);
selectVal = selectVal.map((option) => option.value);
}
onChange && onChange(selectVal);
} else {
@ -838,13 +899,13 @@ export const InputSelect = forwardRef(({
...otherProps,
...props,
};
if(!controlProps.creatable) {
if (!controlProps.creatable) {
return (
<Select ref={ref} {...commonProps}/>
<Select ref={ref} {...commonProps} />
);
} else {
return (
<CreatableSelect ref={ref} {...commonProps}/>
<CreatableSelect ref={ref} {...commonProps} />
);
}
});
@ -863,10 +924,10 @@ InputSelect.propTypes = {
export function FormInputSelect({
hasError, required, className, label, helpMessage, testcid, ...props}) {
hasError, required, className, label, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputSelect ref={props.inputRef} {...props}/>
<InputSelect ref={props.inputRef} {...props} />
</FormInput>
);
}
@ -881,7 +942,7 @@ FormInputSelect.propTypes = {
};
/* React wrapper on color pickr */
export function InputColor({value, controlProps, disabled, onChange, currObj}) {
export function InputColor({ value, controlProps, disabled, onChange, currObj }) {
const pickrOptions = {
showPalette: true,
allowEmpty: true,
@ -896,19 +957,19 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
const pickrObj = useRef();
const classes = useStyles();
const setColor = (newVal)=>{
const setColor = (newVal) => {
pickrObj.current &&
pickrObj.current.setColor((_.isUndefined(newVal) || newVal == '') ? pickrOptions.defaultColor : newVal);
};
const destroyPickr = ()=>{
if(pickrObj.current) {
const destroyPickr = () => {
if (pickrObj.current) {
pickrObj.current.destroy();
pickrObj.current = null;
}
};
const initPickr = ()=>{
const initPickr = () => {
/* pickr does not have way to update options, need to
destroy and recreate pickr to reflect options */
destroyPickr();
@ -920,7 +981,7 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
swatches: [
'#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0',
'#f0f', '#f4cccc', '#fce5cd', '#d0e0e3', '#cfe2f3', '#ead1dc', '#ea9999',
'#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666','#93c47d', '#76a5af', '#c27ba0',
'#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666', '#93c47d', '#76a5af', '#c27ba0',
'#f1c232', '#6aa84f', '#45818e', '#a64d79', '#bf9000', '#0c343d', '#4c1130',
],
position: pickrOptions.position,
@ -941,20 +1002,20 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
setColor(value);
disabled && instance.disable();
const {lastColor} = instance.getRoot().preview;
const {clear} = instance.getRoot().interaction;
const { lastColor } = instance.getRoot().preview;
const { clear } = instance.getRoot().interaction;
/* Cycle the keyboard navigation within the color picker */
clear.addEventListener('keydown', (e)=>{
if(e.keyCode === 9) {
clear.addEventListener('keydown', (e) => {
if (e.keyCode === 9) {
e.preventDefault();
e.stopPropagation();
lastColor.focus();
}
});
lastColor.addEventListener('keydown', (e)=>{
if(e.keyCode === 9 && e.shiftKey) {
lastColor.addEventListener('keydown', (e) => {
if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault();
e.stopPropagation();
clear.focus();
@ -965,32 +1026,32 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
}).on('change', (color) => {
onChange && onChange(color.toHEXA().toString());
}).on('show', (color, instance) => {
const {palette} = instance.getRoot().palette;
const { palette } = instance.getRoot().palette;
palette.focus();
}).on('hide', (instance) => {
const button = instance.getRoot().button;
button.focus();
});
if(currObj) {
if (currObj) {
currObj(pickrObj.current);
}
};
useEffect(()=>{
useEffect(() => {
initPickr();
return ()=>{
return () => {
destroyPickr();
};
}, [...Object.values(pickrOptions)]);
useEffect(()=>{
if(pickrObj.current) {
useEffect(() => {
if (pickrObj.current) {
setColor(value);
}
}, [value]);
let btnStyles = {backgroundColor: value};
let btnStyles = { backgroundColor: value };
return (
<PgIconButton ref={eleRef} title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}
icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
@ -1006,11 +1067,11 @@ InputColor.propTypes = {
};
export function FormInputColor({
hasError, required, className, label, helpMessage, testcid, ...props}) {
hasError, required, className, label, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputColor {...props}/>
<InputColor {...props} />
</FormInput>
);
}
@ -1023,9 +1084,9 @@ FormInputColor.propTypes = {
testcid: PropTypes.string,
};
export function PlainString({controlProps, value}) {
export function PlainString({ controlProps, value }) {
let finalValue = value;
if(controlProps?.formatter) {
if (controlProps?.formatter) {
finalValue = controlProps.formatter.fromRaw(finalValue);
}
return <span>{finalValue}</span>;
@ -1035,7 +1096,7 @@ PlainString.propTypes = {
value: PropTypes.any,
};
export function FormNote({text, className}) {
export function FormNote({ text, className }) {
const classes = useStyles();
return (
<Box className={className}>
@ -1051,7 +1112,7 @@ FormNote.propTypes = {
className: CustomPropTypes.className,
};
const useStylesFormFooter = makeStyles((theme)=>({
const useStylesFormFooter = makeStyles((theme) => ({
root: {
padding: theme.spacing(0.5),
position: 'absolute',
@ -1108,7 +1169,7 @@ const useStylesFormFooter = makeStyles((theme)=>({
export function FormFooterMessage(props) {
const classes = useStylesFormFooter();
if(!props.message) {
if (!props.message) {
return <></>;
}
return (
@ -1122,15 +1183,81 @@ FormFooterMessage.propTypes = {
message: PropTypes.string,
};
export function NotifierMessage({type=MESSAGE_TYPE.SUCCESS, message, closable=true, onClose=()=>{/*This is intentional (SonarQube)*/}}) {
const useStylesKeyboardShortcut = makeStyles(() => ({
customRow: {
paddingTop: 5
}
}));
export function FormInputKeyboardShortcut({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
const cid = _.uniqueId('c');
const helpid = `h${cid}`;
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>
);
}
FormInputKeyboardShortcut.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
onChange: PropTypes.func
};
export function FormInputQueryThreshold({ 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}>
<QueryThresholds cid={cid} helpid={helpid} onChange={onChange} {...props} />
</FormInput>
);
}
FormInputQueryThreshold.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
onChange: PropTypes.func
};
export function FormInputThemes({ 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} />
</FormInput>
);
}
FormInputThemes.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
onChange: PropTypes.func
};
export function NotifierMessage({ type = MESSAGE_TYPE.SUCCESS, message, closable = true, onClose = () => {/*This is intentional (SonarQube)*/ } }) {
const classes = useStylesFormFooter();
return (
<Box className={clsx(classes.container, classes[`container${type}`])}>
<FormIcon type={type} className={classes[`icon${type}`]}/>
<FormIcon type={type} className={classes[`icon${type}`]} />
<Box className={classes.message}>{message}</Box>
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
<FormIcon close={true}/>
<FormIcon close={true} />
</IconButton>}
</Box>
);

View File

@ -0,0 +1,130 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import { makeStyles, Grid, Typography, Box } from '@material-ui/core';
import React from 'react';
import { InputCheckbox, InputText } from './FormComponents';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme) => ({
inputLabel: {
textAlign: 'center',
padding: 2,
paddingLeft: 10
},
inputCheckboxClass: {
border: '1px solid',
borderRadius: theme.shape.borderRadius,
padding: 3
}
}));
export default function KeyboardShortcuts({ value, onChange, fields }) {
const classes = useStyles();
const keyCid = _.uniqueId('c');
const keyhelpid = `h${keyCid}`;
const shiftCid = _.uniqueId('c');
const shifthelpid = `h${shiftCid}`;
const ctrlCid = _.uniqueId('c');
const ctrlhelpid = `h${ctrlCid}`;
const altCid = _.uniqueId('c');
const althelpid = `h${altCid}`;
const onKeyDown = (e) => {
let newVal = { ...value };
let _val = e.key;
if (e.keyCode == 32) {
_val = 'Space';
}
newVal.key = {
char: _val,
key_code: e.keyCode
};
onChange(newVal);
};
const onShiftChange = (e) => {
let newVal = { ...value };
newVal.shift = e.target.checked;
onChange(newVal);
};
const onCtrlChange = (e) => {
let newVal = { ...value };
newVal.ctrl = e.target.checked;
onChange(newVal);
};
const onAltChange = (e) => {
let newVal = { ...value };
newVal.alt = e.target.checked;
onChange(newVal);
};
return (
<Grid
container
direction="row"
justifyContent="center"
alignItems="center">
{fields.map(element => {
let ctrlProps = {
label: element.label
};
if (element.type == 'keyCode') {
return <Grid item container lg={4} md={4} sm={4} xs={12}>
<Grid item lg={4} md={4} sm={4} xs={12} className={classes.inputLabel}>
<Typography>{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={
{
onKeyDown: onKeyDown,
}
}></InputText>
</Grid>
</Grid>;
} else if (element.name == 'shift') {
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={shiftCid} helpid={shifthelpid} value={value?.shift}
controlProps={ctrlProps}
onChange={onShiftChange} labelPlacement="end" ></InputCheckbox>
</Box>
</Grid>;
} else if (element.name == 'control') {
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={ctrlCid} helpid={ctrlhelpid} value={value?.ctrl}
controlProps={ctrlProps}
onChange={onCtrlChange} labelPlacement="end" ></InputCheckbox>
</Box>
</Grid>;
} else if (element.name == 'alt') {
return <Grid item lg={3} md={3} sm={3} xs={12} className={classes.inputLabel}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={altCid} helpid={althelpid} value={value?.alt}
controlProps={ctrlProps}
onChange={onAltChange} labelPlacement="end" ></InputCheckbox>
</Box>
</Grid>;
}
})}
</Grid>
);
}
KeyboardShortcuts.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
controlProps: PropTypes.object,
fields: PropTypes.array
};

View File

@ -0,0 +1,90 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import _ from 'lodash';
import { FormGroup, makeStyles, Grid, Typography } from '@material-ui/core';
import React from 'react';
import { InputText } from './FormComponents';
import PropTypes from 'prop-types';
const useStyles = makeStyles(() => ({
formControlLabel: {
padding: '3px',
},
formInput: {
marginLeft: '5px'
},
formCheckboxControl: {
padding: '3px',
border: '1px solid',
borderRadius: '0.25rem',
},
formGroup: {
padding: '5px'
},
contentTextAlign: {
textAlign: 'center'
},
contentStyle: {
paddingLeft: 10,
}
}));
export default function QueryThresholds({ value, onChange }) {
const classes = useStyles();
const warningCid = _.uniqueId('c');
const warninghelpid = `h${warningCid}`;
const alertCid = _.uniqueId('c');
const alerthelpid = `h${alertCid}`;
const onWarningChange = (val) => {
let new_val = { ...value };
new_val['warning'] = val;
onChange(new_val);
};
const onAlertChange = (val) => {
let new_val = { ...value };
new_val['alert'] = val;
onChange(new_val);
};
return (
<FormGroup>
<Grid
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Grid item lg={2} md={2} sm={2} xs={12}>
<Typography>{gettext('Warning')}</Typography>
</Grid>
<Grid item lg={2} md={2} sm={2} xs={12}>
<InputText cid={warningCid} helpid={warninghelpid} type='numeric' value={value?.warning} onChange={onWarningChange} />
</Grid>
<Grid item lg={2} md={2} sm={2} xs={12} className={classes.contentTextAlign}>
<Typography>{gettext('Alert')}</Typography>
</Grid>
<Grid item lg={2} md={2} sm={2} xs={12}>
<InputText cid={alertCid} helpid={alerthelpid} type='numeric' value={value?.alert} onChange={onAlertChange} />
</Grid>
<Grid item lg={4} md={4} sm={4} xs={12} className={classes.contentStyle}>
<Typography>{gettext('(in minuts)')}</Typography>
</Grid>
</Grid>
</FormGroup >
);
}
QueryThresholds.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
};

View File

@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import { makeStyles, Grid } from '@material-ui/core';
import React, { useState } from 'react';
import {InputSelect } from './FormComponents';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles(() => ({
preview: {
paddingTop: 10
}
}));
export default function Themes({onChange, ...props}) {
const classes = useStyles();
const [previewSrc, setPreviewSrc] = useState(null);
const themeChange = (e) => {
props.options.forEach((opt)=> {
if(opt.value == e) {
setPreviewSrc(opt.preview_src);
}
});
onChange(e);
};
return (
<Grid
container
direction="column"
justifyContent="center">
<Grid item lg={12} md={12} sm={12} xs={12}>
<InputSelect ref={props.inputRef} onChange={themeChange} {...props} />
</Grid>
<Grid item lg={12} md={12} sm={12} xs={12} className={classes.preview}>
<img className='img-fluid mx-auto d-block border' src={previewSrc} alt={gettext('Preview not available...')} />
</Grid>
</Grid>
);
}
Themes.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
controlProps: PropTypes.object,
fields: PropTypes.array,
options: PropTypes.array,
inputRef: CustomPropTypes.ref
};

View File

@ -8,8 +8,8 @@
//////////////////////////////////////////////////////////////
import { Box, Dialog, DialogContent, DialogTitle, makeStyles, Paper } from '@material-ui/core';
import React from 'react';
import {getEpoch} from 'sources/utils';
import React, { useState } from 'react';
import { getEpoch } from 'sources/utils';
import { DefaultButton, PgIconButton, PrimaryButton } from '../components/Buttons';
import Draggable from 'react-draggable';
import CloseIcon from '@material-ui/icons/CloseRounded';
@ -19,13 +19,15 @@ import gettext from 'sources/gettext';
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';
const ModalContext = React.createContext({});
export function useModal() {
return React.useContext(ModalContext);
}
const useAlertStyles = makeStyles((theme)=>({
const useAlertStyles = makeStyles((theme) => ({
footer: {
display: 'flex',
justifyContent: 'flex-end',
@ -37,11 +39,11 @@ const useAlertStyles = makeStyles((theme)=>({
}
}));
function AlertContent({text, confirm, okLabel=gettext('OK'), cancelLabel=gettext('Cancel'), onOkClick, onCancelClick}) {
function AlertContent({ text, confirm, okLabel = gettext('OK'), cancelLabel = gettext('Cancel'), onOkClick, onCancelClick }) {
const classes = useAlertStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof(text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box flexGrow="1" p={2}>{typeof (text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.footer}>
{confirm &&
<DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} >{cancelLabel}</DefaultButton>
@ -60,10 +62,10 @@ AlertContent.propTypes = {
cancelLabel: PropTypes.string,
};
function alert(title, text, onOkClick, okLabel=gettext('OK')){
function alert(title, text, onOkClick, okLabel = gettext('OK')) {
// bind the modal provider before calling
this.showModal(title, (closeModal)=>{
const onOkClickClose = ()=>{
this.showModal(title, (closeModal) => {
const onOkClickClose = () => {
onOkClick && onOkClick();
closeModal();
};
@ -73,45 +75,53 @@ function alert(title, text, onOkClick, okLabel=gettext('OK')){
});
}
function confirm(title, text, onOkClick, onCancelClick, okLabel=gettext('Yes'), cancelLabel=gettext('No')) {
function confirm(title, text, onOkClick, onCancelClick, okLabel = gettext('Yes'), cancelLabel = gettext('No')) {
// bind the modal provider before calling
this.showModal(title, (closeModal)=>{
const onCancelClickClose = ()=>{
this.showModal(title, (closeModal) => {
const onCancelClickClose = () => {
onCancelClick && onCancelClick();
closeModal();
};
const onOkClickClose = ()=>{
const onOkClickClose = () => {
onOkClick && onOkClick();
closeModal();
};
return (
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={onCancelClickClose} okLabel={okLabel} cancelLabel={cancelLabel}/>
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={onCancelClickClose} okLabel={okLabel} cancelLabel={cancelLabel} />
);
});
}
export default function ModalProvider({children}) {
export default function ModalProvider({ children }) {
const [modals, setModals] = React.useState([]);
const showModal = (title, content, modalOptions)=>{
const showModal = (title, content, modalOptions) => {
let id = getEpoch().toString() + Math.random();
setModals((prev)=>[...prev, {
setModals((prev) => [...prev, {
id: id,
title: title,
content: content,
...modalOptions,
}]);
};
const closeModal = (id)=>{
setModals((prev)=>{
return prev.filter((o)=>o.id!=id);
const closeModal = (id) => {
setModals((prev) => {
return prev.filter((o) => o.id != id);
});
};
const fullScreenModal = (fullScreen) => {
setModals((prev) => [...prev, {
fullScreen: fullScreen,
}]);
};
const modalContextBase = {
showModal: showModal,
closeModal: closeModal,
fullScreenModal: fullScreenModal
};
const modalContext = React.useMemo(()=>({
const modalContext = React.useMemo(() => ({
...modalContextBase,
confirm: confirm.bind(modalContextBase),
alert: alert.bind(modalContextBase)
@ -119,8 +129,8 @@ export default function ModalProvider({children}) {
return (
<ModalContext.Provider value={modalContext}>
{children}
{modals.map((modalOptions, i)=>(
<ModalContainer key={i} {...modalOptions}/>
{modals.map((modalOptions, i) => (
<ModalContainer key={i} {...modalOptions} />
))}
</ModalContext.Provider>
);
@ -130,30 +140,118 @@ ModalProvider.propTypes = {
children: CustomPropTypes.children,
};
const dialogStyle = makeStyles((theme) => ({
dialog: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid ' + theme.otherVars.inputBorderColor,
borderRadius: theme.shape.borderRadius,
},
}));
function PaperComponent(props) {
let classes = dialogStyle();
let [dialogPosition, setDialogPosition] = useState(null);
let resizeable = props.isresizeable == 'true' ? true : false;
return (
<Draggable cancel={'[class*="MuiDialogContent-root"]'}>
<Paper {...props} style={{minWidth: '600px'}} />
</Draggable>
props.isresizeable == 'true' ?
<Rnd
size={props.isfullscreen == 'true' && { width: '100%', height: '100%' }}
className={classes.dialog}
default={{
x: 300,
y: 100,
...(props.width && { width: props.width }),
...(props.height && { height: props.height }),
}}
{...(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 }}
onDragStop={(e, position) => {
if (props.isfullscreen !== 'true') {
setDialogPosition({
...position,
});
}
}}
onResize={(e, direction, ref, delta, position) => {
setDialogPosition({
...position,
});
}}
>
<Paper {...props} style={{ width: '100%', height: '100%', maxHeight: '100%', maxWidth: '100%' }} />
</Rnd>
:
<Draggable cancel={'[class*="MuiDialogContent-root"]'}>
<Paper {...props} style={{ minWidth: '600px' }} />
</Draggable>
);
}
function ModalContainer({id, title, content}) {
PaperComponent.propTypes = {
isfullscreen: PropTypes.string,
isresizeable: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
};
const useModalStyles = makeStyles(() => ({
titleBar: {
display: 'flex',
flexGrow: 1
},
title: {
flexGrow: 1
},
icon: {
fill: 'currentColor',
width: '1em',
height: '1em',
display: 'inline-block',
fontSize: '1.5rem',
transition: 'none',
flexShrink: 0,
userSelect: 'none',
}
}));
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false }) {
let useModalRef = useModal();
let closeModal = ()=>useModalRef.closeModal(id);
const classes = useModalStyles();
let closeModal = () => useModalRef.closeModal(id);
const [isfullScreen, setIsFullScreen] = useState(fullScreen);
return (
<Theme>
<Dialog
open={true}
onClose={closeModal}
PaperComponent={PaperComponent}
PaperProps={{ 'isfullscreen': isfullScreen.toString(), 'isresizeable': isResizeable.toString(), width: dialogWidth, height: dialogHeight }}
fullScreen={isfullScreen}
fullWidth={isFullWidth}
disableBackdropClick
>
<DialogTitle>
<Box marginRight="0.25rem">{title}</Box>
<Box marginLeft="auto"><PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={closeModal}/></Box>
<Box className={classes.titleBar}>
<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>
}
{
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('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={closeModal} /></Box>
</Box>
</DialogTitle>
<DialogContent>
<DialogContent height="100%">
{content(closeModal)}
</DialogContent>
</Dialog>
@ -164,4 +262,11 @@ ModalContainer.propTypes = {
id: PropTypes.string,
title: CustomPropTypes.children,
content: PropTypes.func,
fullScreen: PropTypes.bool,
maxWidth: PropTypes.string,
isFullWidth: PropTypes.bool,
showFullScreen: PropTypes.bool,
isResizeable: PropTypes.bool,
dialogHeight: PropTypes.number,
dialogWidth: PropTypes.number,
};

View File

@ -8,6 +8,13 @@
//////////////////////////////////////////////////////////////
import { useSnackbar, SnackbarProvider, SnackbarContent } from 'notistack';
import { makeStyles } from '@material-ui/core/styles';
import {Box} from '@material-ui/core';
import CloseIcon from '@material-ui/icons/CloseRounded';
import { DefaultButton, PrimaryButton } from '../components/Buttons';
import HTMLReactParser from 'html-react-parser';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import Theme from 'sources/Theme';
@ -76,6 +83,41 @@ FinalNotifyContent.propTypes = {
children: CustomPropTypes.children,
};
const useModalStyles = makeStyles((theme)=>({
footer: {
display: 'flex',
justifyContent: 'flex-end',
padding: '0.5rem',
...theme.mixins.panelBorder.top,
},
margin: {
marginLeft: '0.25rem',
}
}));
function AlertContent({text, confirm, okLabel=gettext('OK'), cancelLabel=gettext('Cancel'), onOkClick, onCancelClick}) {
const classes = useModalStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{HTMLReactParser(text)}</Box>
<Box className={classes.footer}>
{confirm &&
<DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} >{cancelLabel}</DefaultButton>
}
<PrimaryButton className={classes.margin} startIcon={<CheckRoundedIcon />} onClick={onOkClick} autoFocus={true} >{okLabel}</PrimaryButton>
</Box>
</Box>
);
}
AlertContent.propTypes = {
text: PropTypes.string,
confirm: PropTypes.bool,
onOkClick: PropTypes.func,
onCancelClick: PropTypes.func,
okLabel: PropTypes.string,
cancelLabel: PropTypes.string,
};
var Notifier = {
success(msg, autoHideDuration = AUTO_HIDE_DURATION) {
this._callNotify(msg, MESSAGE_TYPE.SUCCESS, autoHideDuration);
@ -195,11 +237,11 @@ var Notifier = {
}
modalRef.confirm(title, text, onOkClick, onCancelClick, okLabel, cancelLabel);
},
showModal(title, content) {
showModal: (title, content, modalOptions) => {
if(!modalInitialized) {
initializeModalProvider();
}
modalRef.showModal(title, content);
modalRef.showModal(title, content, modalOptions);
}
};

View File

@ -0,0 +1,247 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import * as BrowserFS from 'browserfs'
import pgAdmin from 'sources/pgadmin';
import _ from 'underscore';
import { FileType } from 'react-aspen'
import { findInTree } from './tree';
export class ManagePreferenceTreeNodes {
constructor(data) {
this.tree = {}
this.tempTree = new TreeNode(undefined, {});
this.treeData = data;
}
public init = (_root: string) => new Promise((res, rej) => {
let node = { parent: null, children: [], data: null };
this.tree = {};
this.tree[_root] = { name: 'root', type: FileType.Directory, metadata: node };
res();
})
public updateNode = (_path, _data) => new Promise((res, rej) => {
const item = this.findNode(_path);
if (item) {
item.name = _data.label;
item.metadata.data = _data;
}
res(true);
})
public removeNode = async (_path, _removeOnlyChild) => {
const item = this.findNode(_path);
if (item && item.parentNode) {
item.children = [];
item.parentNode.children.splice(item.parentNode.children.indexOf(item), 1);
}
return true;
};
findNode(path) {
if (path === null || path === undefined || path.length === 0 || path == '/preferences') {
return this.tempTree;
}
console.log('Path', path)
return findInTree(this.tempTree, path);
}
public addNode = (_parent: string, _path: string, _data: []) => new Promise((res, rej) => {
_data.type = _data.inode ? FileType.Directory : FileType.File;
_data._label = _data.label;
_data.label = _.escape(_data.label);
_data.is_collection = isCollectionNode(_data._type);
let nodeData = { parent: _parent, children: _data?.children ? _data.children : [], data: _data };
let tmpParentNode = this.findNode(_parent);
let treeNode = new TreeNode(_data.id, _data, {}, tmpParentNode, nodeData, _data.type);
if (tmpParentNode !== null && tmpParentNode !== undefined) tmpParentNode.children.push(treeNode);
res(treeNode);
})
public readNode = (_path: string) => new Promise<string[]>((res, rej) => {
let temp_tree_path = _path,
node = this.findNode(_path);
node.children = [];
if (node && node.children.length > 0) {
if (!node.type === FileType.File) {
rej("It's a leaf node")
}
else {
if (node?.children.length != 0) res(node.children)
}
}
var self = this;
async function loadData() {
const Path = BrowserFS.BFSRequire('path')
const fill = async (tree) => {
for (let idx in tree) {
const _node = tree[idx]
const _pathl = Path.join(_path, _node.id)
await self.addNode(temp_tree_path, _pathl, _node);
}
}
if (node && !_.isUndefined(node.id)) {
let _data = self.treeData.find((el) => el.id == node.id);
let subNodes = [];
_data.childrenNodes.forEach(element => {
subNodes.push(element)
});
await fill(subNodes);
} else {
await fill(self.treeData);
}
if (node?.children.length > 0) return res(node.children);
else return res(null);
}
loadData();
})
}
export class TreeNode {
constructor(id, data, domNode, parent, metadata, type) {
this.id = id;
this.data = data;
this.setParent(parent);
this.children = [];
this.domNode = domNode;
this.metadata = metadata;
this.name = metadata ? metadata.data.label : "";
this.type = type ? type : undefined;
}
hasParent() {
return this.parentNode !== null && this.parentNode !== undefined;
}
parent() {
return this.parentNode;
}
setParent(parent) {
this.parentNode = parent;
this.path = this.id;
if (this.id)
if (parent !== null && parent !== undefined && parent.path !== undefined) {
this.path = parent.path + '/' + this.id;
} else {
this.path = '/preferences/' + this.id;
}
}
getData() {
if (this.data === undefined) {
return undefined;
} else if (this.data === null) {
return null;
}
return Object.assign({}, this.data);
}
getHtmlIdentifier() {
return this.domNode;
}
/*
* Find the ancestor with matches this condition
*/
ancestorNode(condition) {
let node = this;
while (node.hasParent()) {
node = node.parent();
if (condition(node)) {
return node;
}
}
return null;
}
/**
* Given a condition returns true if the current node
* or any of the parent nodes condition result is true
*/
anyFamilyMember(condition) {
if (condition(this)) {
return true;
}
return this.ancestorNode(condition) !== null;
}
anyParent(condition) {
return this.ancestorNode(condition) !== null;
}
reload(tree) {
return new Promise((resolve) => {
this.unload(tree)
.then(() => {
tree.setInode(this.domNode);
tree.deselect(this.domNode);
setTimeout(() => {
tree.selectNode(this.domNode);
}, 0);
resolve();
});
});
}
unload(tree) {
return new Promise((resolve, reject) => {
this.children = [];
tree.unload(this.domNode)
.then(
() => {
resolve(true);
},
() => {
reject();
});
});
}
open(tree, suppressNoDom) {
return new Promise((resolve, reject) => {
if (suppressNoDom && (this.domNode == null || typeof (this.domNode) === 'undefined')) {
resolve(true);
} else if (tree.isOpen(this.domNode)) {
resolve(true);
} else {
tree.open(this.domNode).then(val => resolve(true), err => reject(true));
}
});
}
}
export function isCollectionNode(node) {
if (pgAdmin.Browser.Nodes && node in pgAdmin.Browser.Nodes) {
if (pgAdmin.Browser.Nodes[node].is_collection !== undefined) return pgAdmin.Browser.Nodes[node].is_collection;
else return false;
}
return false;
}

View File

@ -0,0 +1,53 @@
/////////////////////////////////////////////////////////////
//
// 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

@ -14,52 +14,68 @@ import pgAdmin from 'sources/pgadmin';
import { FileType } from 'react-aspen';
import { TreeNode } from './tree_nodes';
import {isValidData} from 'sources/utils';
import { isValidData } from 'sources/utils';
function manageTreeEvents(event, eventName, item) {
let d = item ? item._metadata.data: [];
let d = item ? item._metadata.data : [];
let node_metadata = item ? item._metadata : {};
let node;
let obj = pgAdmin.Browser;
if (d && obj.Nodes[d._type]) {
node = obj.Nodes[d._type];
// If the Browser tree is not initialised yet
if (obj.tree === null) return;
if (eventName == 'dragstart') {
obj.tree.handleDraggable(event, item);
}
if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') {
obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data') ,item, item.parent.path);
}
if (_.isObject(node.callbacks) &&
eventName in node.callbacks &&
typeof node.callbacks[eventName] == 'function' &&
!node.callbacks[eventName].apply(
node, [item, d, obj, [], eventName])) {
return true;
}
/* Raise tree events for the nodes */
// Events for preferences tree.
if (node_metadata.parent && node_metadata.parent.includes('/preferences') && obj.ptree.tree.type == 'preferences') {
try {
node.trigger(
'browser-node.' + eventName, node, item, d
);
obj.Events.trigger(
'pgadmin-browser:tree:' + eventName, item, d, node
'preferences:tree:' + eventName, item, d
);
} catch (e) {
console.warn(e.stack || e);
return false;
}
} else {
// Events for browser tree.
if (d && obj.Nodes[d._type]) {
node = obj.Nodes[d._type];
// If the Browser tree is not initialised yet
if (obj.tree === null) return;
if (eventName == 'dragstart') {
obj.tree.handleDraggable(event, item);
}
if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') {
obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data'), item, item.parent.path);
}
if (_.isObject(node.callbacks) &&
eventName in node.callbacks &&
typeof node.callbacks[eventName] == 'function' &&
!node.callbacks[eventName].apply(
node, [item, d, obj, [], eventName])) {
return true;
}
/* Raise tree events for the nodes */
try {
node.trigger(
'browser-node.' + eventName, node, item, d
);
obj.Events.trigger(
'pgadmin-browser:tree:' + eventName, item, d, node
);
} catch (e) {
console.warn(e.stack || e);
return false;
}
}
}
return true;
}
export class Tree {
constructor(tree, manageTree, pgBrowser) {
constructor(tree, manageTree, pgBrowser, type) {
this.tree = tree;
this.tree.type = type ? type : 'browser';
this.tree.onTreeEvents(manageTreeEvents);
this.rootNode = manageTree.tempTree;
@ -102,12 +118,12 @@ export class Tree {
}
next(item) {
if(item) {
if (item) {
let parent = this.parent(item);
if(parent && parent.children.length > 0) {
if (parent && parent.children.length > 0) {
let idx = parent.children.indexOf(item);
if(idx !== -1 && parent.children.length !== idx+1) {
return parent.children[idx+1];
if (idx !== -1 && parent.children.length !== idx + 1) {
return parent.children[idx + 1];
}
}
}
@ -115,12 +131,12 @@ export class Tree {
}
prev(item) {
if(item) {
if (item) {
let parent = this.parent(item);
if(parent && parent.children.length > 0) {
if (parent && parent.children.length > 0) {
let idx = parent.children.indexOf(item);
if(idx !== -1 && idx !== 0) {
return parent.children[idx-1];
if (idx !== -1 && idx !== 0) {
return parent.children[idx - 1];
}
}
}
@ -136,7 +152,7 @@ export class Tree {
await item.ensureLoaded();
}
async ensureVisible(item){
async ensureVisible(item) {
await this.tree.ensureVisible(item);
}
@ -153,11 +169,11 @@ export class Tree {
await this.tree.toggleDirectory(item);
}
async select(item, ensureVisible=false, align='auto') {
async select(item, ensureVisible = false, align = 'auto') {
await this.tree.setActiveFile(item, ensureVisible, align);
}
async selectNode(item, ensureVisible=false, align='auto') {
async selectNode(item, ensureVisible = false, align = 'auto') {
this.tree.setActiveFile(item, ensureVisible, align);
}
@ -180,18 +196,18 @@ export class Tree {
// TBD
}
async setLabel(item, label) {
if(item) {
if (item) {
await this.tree.setLabel(item, label);
}
}
async setInode(item) {
if(item._children) item._children = null;
if (item._children) item._children = null;
await this.tree.closeDirectory(item);
}
async setId(item, data) {
if(item) {
if (item) {
item.getMetadata('data').id = data.id;
}
}
@ -269,7 +285,7 @@ export class Tree {
siblings(item) {
if (this.hasParent(item)) {
let _siblings = this.parent(item).children.filter((_item) => _item.path !== item.path);
if (typeof(_siblings) !== 'object') return [_siblings];
if (typeof (_siblings) !== 'object') return [_siblings];
else return _siblings;
}
return [];
@ -294,7 +310,7 @@ export class Tree {
}
itemData(item) {
return (item !== undefined && item !== null && item.getMetadata('data') !== undefined) ? item._metadata.data : [];
return (item !== undefined && item !== null && item.getMetadata('data') !== undefined) ? item._metadata.data : [];
}
getData(item) {
@ -323,18 +339,18 @@ export class Tree {
findNodeWithToggle(path) {
let tree = this;
if(path == null || !Array.isArray(path)) {
if (path == null || !Array.isArray(path)) {
return Promise.reject();
}
path = '/browser/' + path.join('/');
let onCorrectPath = function(matchPath) {
let onCorrectPath = function (matchPath) {
return (matchPath !== undefined && path !== undefined
&& (path.startsWith(matchPath) || path === matchPath));
};
return (function findInNode(currentNode) {
return new Promise((resolve, reject)=>{
return new Promise((resolve, reject) => {
if (path === null || path === undefined || path.length === 0) {
resolve(null);
}
@ -347,18 +363,18 @@ export class Tree {
resolve(currentNode);
} else {
tree.open(currentNode)
.then(()=>{
.then(() => {
let children = currentNode.children;
for (let i = 0, length = children.length; i < length; i++) {
let childNode = children[i];
if(onCorrectPath(childNode.path)) {
if (onCorrectPath(childNode.path)) {
resolve(findInNode(childNode));
return;
}
}
reject(null);
})
.catch(()=>{
.catch(() => {
reject(null);
});
}
@ -368,7 +384,7 @@ export class Tree {
findNodeByDomElement(domElement) {
const path = domElement.path;
if(!path || !path[0]) {
if (!path || !path[0]) {
return undefined;
}
@ -390,7 +406,7 @@ export class Tree {
createOrUpdateNode(id, data, parent, domNode) {
let oldNodePath = id;
if(parent !== null && parent !== undefined && parent.path !== undefined && parent.path != '/browser') {
if (parent !== null && parent !== undefined && parent.path !== undefined && parent.path != '/browser') {
oldNodePath = parent.path + '/' + id;
}
const oldNode = this.findNode(oldNodePath);
@ -456,14 +472,14 @@ export class Tree {
* cur is selection range of text after dropping. If returned as
* string, by default cursor will be set to the end of text
*/
registerDraggableType(typeOrTypeDict, dropDetailsFunc=null) {
if(typeof typeOrTypeDict == 'object') {
Object.keys(typeOrTypeDict).forEach((type)=>{
registerDraggableType(typeOrTypeDict, dropDetailsFunc = null) {
if (typeof typeOrTypeDict == 'object') {
Object.keys(typeOrTypeDict).forEach((type) => {
this.registerDraggableType(type, typeOrTypeDict[type]);
});
} else {
if(dropDetailsFunc != null) {
typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type)=>{
if (dropDetailsFunc != null) {
typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type) => {
this.draggableTypes[type] = dropDetailsFunc;
});
}
@ -471,7 +487,7 @@ export class Tree {
}
getDraggable(type) {
if(this.draggableTypes[type]) {
if (this.draggableTypes[type]) {
return this.draggableTypes[type];
} else {
return null;
@ -482,7 +498,7 @@ export class Tree {
let data = item.getMetadata('data');
let dropDetailsFunc = this.getDraggable(data._type);
if(dropDetailsFunc != null) {
if (dropDetailsFunc != null) {
/* addEventListener is used here because import jquery.drag.event
* overrides the dragstart event set using element.on('dragstart')
@ -490,20 +506,20 @@ export class Tree {
*/
let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item));
if(typeof dropDetails == 'string') {
if (typeof dropDetails == 'string') {
dropDetails = {
text:dropDetails,
cur:{
from:dropDetails.length,
text: dropDetails,
cur: {
from: dropDetails.length,
to: dropDetails.length,
},
};
} else {
if(!dropDetails.cur) {
if (!dropDetails.cur) {
dropDetails = {
...dropDetails,
cur:{
from:dropDetails.text.length,
cur: {
from: dropDetails.text.length,
to: dropDetails.text.length,
},
};
@ -512,14 +528,14 @@ export class Tree {
e.dataTransfer.setData('text', JSON.stringify(dropDetails));
/* Required by Firefox */
if(e.dataTransfer.dropEffect) {
if (e.dataTransfer.dropEffect) {
e.dataTransfer.dropEffect = 'move';
}
/* setDragImage is not supported in IE. We leave it to
* its default look and feel
*/
if(e.dataTransfer.setDragImage) {
if (e.dataTransfer.setDragImage) {
let dragItem = $(`
<div class="drag-tree-node">
<span>${_.escape(dropDetails.text)}</span>
@ -576,4 +592,4 @@ export function findInTree(rootNode, path) {
let isValidTreeNodeData = isValidData;
export {isValidTreeNodeData};
export { isValidTreeNodeData };

View File

@ -1,5 +1,8 @@
/* Overrides alertify js headers */
.alertify {
z-index: 3000;
position: fixed;
.ajs-header {
padding: 6px 10px !important;
//margin is calculated with -$alertify-borderremove-margin, adjust the header

View File

@ -197,7 +197,7 @@ def register_query_tool_preferences(self):
options=[{'label': gettext('None'), 'value': 'none'},
{'label': gettext('All'), 'value': 'all'},
{'label': gettext('Strings'), 'value': 'strings'}],
select2={
control_props={
'allowClear': False,
'tags': False
}
@ -209,9 +209,9 @@ def register_query_tool_preferences(self):
category_label=PREF_LABEL_CSV_TXT,
options=[{'label': '"', 'value': '"'},
{'label': '\'', 'value': '\''}],
select2={
control_props={
'allowClear': False,
'tags': True
'tags': False
}
)
@ -223,9 +223,9 @@ def register_query_tool_preferences(self):
{'label': ',', 'value': ','},
{'label': '|', 'value': '|'},
{'label': gettext('Tab'), 'value': '\t'}],
select2={
control_props={
'allowClear': False,
'tags': True
'tags': False
}
)
@ -247,7 +247,7 @@ def register_query_tool_preferences(self):
options=[{'label': gettext('None'), 'value': 'none'},
{'label': gettext('All'), 'value': 'all'},
{'label': gettext('Strings'), 'value': 'strings'}],
select2={
control_props={
'allowClear': False,
'tags': False
}
@ -259,9 +259,9 @@ def register_query_tool_preferences(self):
category_label=PREF_LABEL_RESULTS_GRID,
options=[{'label': '"', 'value': '"'},
{'label': '\'', 'value': '\''}],
select2={
control_props={
'allowClear': False,
'tags': True
'tags': False
}
)
@ -273,9 +273,9 @@ def register_query_tool_preferences(self):
{'label': ',', 'value': ','},
{'label': '|', 'value': '|'},
{'label': gettext('Tab'), 'value': '\t'}],
select2={
control_props={
'allowClear': False,
'tags': True
'tags': False
}
)

View File

@ -66,10 +66,11 @@ class _Preference(object):
self.label = label
self._type = _type
self.help_str = kwargs.get('help_str', None)
self.control_props = kwargs.get('control_props', None)
self.min_val = kwargs.get('min_val', None)
self.max_val = kwargs.get('max_val', None)
self.options = kwargs.get('options', None)
self.select2 = kwargs.get('select2', None)
self.select = kwargs.get('select', None)
self.fields = kwargs.get('fields', None)
self.allow_blanks = kwargs.get('allow_blanks', None)
self.disabled = kwargs.get('disabled', False)
@ -146,10 +147,10 @@ class _Preference(object):
for opt in self.options:
if 'value' in opt and opt['value'] == res.value:
return True, res.value
if self.select2 and self.select2['tags']:
if self.select and self.select['tags']:
return True, res.value
return True, self.default
if self._type == 'select2':
if self._type == 'select':
if res.value:
res.value = res.value.replace('[', '')
res.value = res.value.replace(']', '')
@ -190,7 +191,7 @@ class _Preference(object):
has_value = next((True for opt in self.options
if 'value' in opt and opt['value'] == value),
False)
assert (has_value or (self.select2 and self.select2['tags']))
assert (has_value or (self.select and self.select['tags']))
elif self._type == 'date':
value = parser_map[self._type](value).date()
else:
@ -248,10 +249,11 @@ class _Preference(object):
'label': self.label or self.name,
'type': self._type,
'help_str': self.help_str,
'control_props': self.control_props,
'min_val': self.min_val,
'max_val': self.max_val,
'options': self.options,
'select2': self.select2,
'select': self.select,
'value': self.get(),
'fields': self.fields,
'disabled': self.disabled,
@ -393,7 +395,7 @@ class Preferences(object):
return res
def register(
self, category, name, label, _type, default, **kwargs
self, category, name, label, _type, default, **kwargs
):
"""
register
@ -414,7 +416,7 @@ class Preferences(object):
:param options:
:param help_str:
:param category_label:
:param select2: select2 control extra options
:param select: select control extra options
: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.
@ -424,8 +426,9 @@ class Preferences(object):
max_val = kwargs.get('max_val', None)
options = kwargs.get('options', None)
help_str = kwargs.get('help_str', None)
control_props = kwargs.get('control_props', {})
category_label = kwargs.get('category_label', None)
select2 = kwargs.get('select2', None)
select = kwargs.get('select', None)
fields = kwargs.get('fields', None)
allow_blanks = kwargs.get('allow_blanks', None)
disabled = kwargs.get('disabled', False)
@ -440,14 +443,15 @@ class Preferences(object):
assert _type in (
'boolean', 'integer', 'numeric', 'date', 'datetime',
'options', 'multiline', 'switch', 'node', 'text', 'radioModern',
'keyboardshortcut', 'select2', 'selectFile', 'threshold'
'keyboardshortcut', 'select', 'selectFile', 'threshold'
), "Type cannot be found in the defined list!"
(cat['preferences'])[name] = res = _Preference(
cat['id'], name, label, _type, default, help_str=help_str,
min_val=min_val, max_val=max_val, options=options,
select2=select2, fields=fields, allow_blanks=allow_blanks,
disabled=disabled, dependents=dependents
select=select, fields=fields, allow_blanks=allow_blanks,
disabled=disabled, dependents=dependents,
control_props=control_props
)
return res
@ -483,7 +487,7 @@ class Preferences(object):
@classmethod
def register_preference(
cls, module, category, name, label, _type, **kwargs
cls, module, category, name, label, _type, **kwargs
):
"""
register
@ -503,6 +507,7 @@ class Preferences(object):
max_val = kwargs.get('max_val', None)
options = kwargs.get('options', None)
help_str = kwargs.get('help_str', None)
control_props = kwargs.get('control_props', None)
module_label = kwargs.get('module_label', None)
category_label = kwargs.get('category_label', None)
@ -516,6 +521,7 @@ class Preferences(object):
return m.register(
category, name, label, _type, default, min_val=min_val,
max_val=max_val, options=options, help_str=help_str,
control_props=control_props,
category_label=category_label
)

View File

@ -0,0 +1,100 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
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 KeyboardShortcuts from '../../../pgadmin/static/js/components/KeyboardShortcuts';
/* MUI Components need to be wrapped in Theme for theme vars */
describe('KeyboardShortcuts', () => {
let mount;
let defult_value = {
'ctrl': true,
'alt': true,
'key': {
'char': 'a',
'key_code': 97
}
};
let fields = [{
type: 'keyCode',
label: 'Key'
}, {
name: 'shift',
label: 'Shift',
type: 'checkbox'
},
{
name: 'control',
label: 'Control',
type: 'checkbox'
},
{
name: 'alt',
label: 'Alt/Option',
type: 'checkbox'
}];
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(() => {
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(() => {
jasmineEnzyme();
});
describe('KeyboardShortcuts', () => {
let ThemedFormInputKeyboardShortcuts = withTheme(KeyboardShortcuts), ctrl;
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'
}}
/>);
});
it('init', () => {
expect(ctrl.find(OutlinedInput).prop('value')).toBe('a');
});
it('Key change', () => {
let onChange = () => {/*This is intentional (SonarQube)*/ };
ctrl.setProps({
controlProps: {
onKeyDown: onChange
}
});
expect(ctrl.find(OutlinedInput).prop('value')).toBe('a');
});
});
});

View File

@ -0,0 +1,86 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
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';
/* MUI Components need to be wrapped in Theme for theme vars */
describe('QueryThresholds', () => {
let mount;
let defult_value = {
'warning': 5,
'alert': 6
};
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(() => {
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(() => {
jasmineEnzyme();
});
describe('QueryThresholds', () => {
let ThemedFormInputQueryThresholds = withTheme(QueryThresholds), ctrl;
beforeEach(() => {
ctrl = mount(
<ThemedFormInputQueryThresholds
testcid="inpCid"
helpMessage="some help message"
/* InputText */
readonly={false}
disabled={false}
maxlength={1}
value={defult_value}
controlProps={{
extraprop: 'test'
}}
/>);
});
it('init Warning', () => {
expect(ctrl.find(OutlinedInput).at(0).prop('value')).toBe(5);
});
it('init Alert', () => {
expect(ctrl.find(OutlinedInput).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);
});
});
});

View File

@ -0,0 +1,96 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
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 { InputSelect } from '../../../pgadmin/static/js/components/FormComponents';
/* MUI Components need to be wrapped in Theme for theme vars */
describe('Themes', () => {
let mount;
let options = [{
value: 'standard',
preview_src: 'sd',
selected: true,
label: 'Standard'
}, {
value: 'dark',
preview_src: 'test',
selected: false,
label: 'Dark'
},
{
value: 'high_contrast',
preview_src: 'hc',
selected: false,
label: 'High Contrast',
}
];
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(() => {
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(() => {
jasmineEnzyme();
});
describe('Themes', () => {
let ThemedFormInputThemes = withTheme(Themes), ctrl, onChange = jasmine.createSpy('onChange');
beforeEach(() => {
ctrl = mount(
<ThemedFormInputThemes
testcid="inpCid"
helpMessage="some help message"
options={options}
onChange={onChange}
value={'standard'}
/>);
});
it('init options', () => {
expect(ctrl.find(InputSelect).at(0).prop('options').length).toBe(3);
});
it('change value', () => {
ctrl.setProps({
value: 'dark',
onChange: onChange,
});
expect(ctrl.find(InputSelect).at(0).prop('value')).toBe('dark');
});
it('onChange', () => {
let select = ctrl.find(InputSelect).at(0);
const input = select.find('input');
input.simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });
input.simulate('keyDown', { key: 'Enter', keyCode: 13 });
ctrl.setProps({
value: 'dark',
onChange: onChange,
});
expect(ctrl.find(InputSelect).at(0).prop('value')).toBe('dark');
});
});
});

View File

@ -0,0 +1,47 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import Notify from '../../../pgadmin/static/js/helpers/Notifier';
import {genericBeforeEach, getEditView} from '../genericFunctions';
import {getBinaryPathSchema} from '../../../pgadmin/browser/server_groups/servers/static/js/binary_path.ui';
describe('BinaryPathschema', ()=>{
let mount;
let schemaObj = getBinaryPathSchema();
let getInitData = ()=>Promise.resolve({});
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
spyOn(Notify, 'alert');
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(()=>{
genericBeforeEach();
});
it('edit', ()=>{
mount(getEditView(schemaObj, getInitData));
});
it('validate path', ()=>{
let validate = _.find(schemaObj.fields, (f)=>f.id=='binaryPath').validate;
let status = validate('');
expect(status).toBe(true);
});
});

View File

@ -282,7 +282,7 @@ var webpackShimConfig = {
'pgadmin.node.user_mapping': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/user_mappings/static/js/user_mapping'),
'pgadmin.node.view': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view'),
'pgadmin.node.row_security_policy': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/row_security_policies/static/js/row_security_policy'),
'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/preferences'),
'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/'),
'pgadmin.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'),
'pgadmin.server.supported_servers': '/browser/server/supported_servers',
'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'),

View File

@ -91,19 +91,19 @@
json5 "^2.1.2"
semver "^6.3.0"
"@babel/eslint-parser@^7.12.13":
version "7.13.8"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.13.8.tgz#6f2bde6b0690fcc0598b4869fc7c8e8b55b17687"
integrity sha512-XewKkiyukrGzMeqToXJQk6hjg2veI9SNQElGzAoAjKxYCLbgcVX4KA2WhoyqMon9N4RMdCZhNTJNOBcp9spsiw==
"@babel/eslint-parser@^7.17.0":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz#eabb24ad9f0afa80e5849f8240d0e5facc2d90d6"
integrity sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==
dependencies:
eslint-scope "5.1.0"
eslint-visitor-keys "^1.3.0"
eslint-scope "^5.1.1"
eslint-visitor-keys "^2.1.0"
semver "^6.3.0"
"@babel/eslint-plugin@^7.12.13":
version "7.13.0"
resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.13.0.tgz#e6d99efcd6b8551adf479e382a47218726179b1b"
integrity sha512-YGwCLc/u/uc3bU+q/fvgRQ62+TkxuyVvdmybK6ElzE49vODp+RnRe16eJzMM7EwvcRPQfQvcOSuGmzfcbZE2+w==
"@babel/eslint-plugin@^7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.17.7.tgz#4ee1d5b29b79130f3bb5a933358376bcbee172b8"
integrity sha512-JATUoJJXSgwI0T8juxWYtK1JSgoLpIGUsCHIv+NMXcUDA2vIe6nvAHR9vnuJgs/P1hOFw7vPwibixzfqBBLIVw==
dependencies:
eslint-rule-composer "^0.3.0"
@ -5223,14 +5223,6 @@ eslint-rule-composer@^0.3.0:
resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"
integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==
eslint-scope@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5"
integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -5251,7 +5243,7 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
eslint-visitor-keys@^2.0.0:
eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
@ -5320,7 +5312,7 @@ esquery@^1.4.0:
dependencies:
estraverse "^5.1.0"
esrecurse@^4.1.0, esrecurse@^4.3.0:
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
@ -5498,6 +5490,11 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-memoize@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
fast-safe-stringify@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
@ -8351,9 +8348,9 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
"pgadmin4-tree@git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422":
"pgadmin4-tree@git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613":
version "1.0.0"
resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422"
resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613"
dependencies:
"@types/classnames" "^2.2.6"
"@types/react" "^16.7.18"
@ -9023,6 +9020,13 @@ rc-util@^5.12.0, rc-util@^5.15.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0
react-is "^16.12.0"
shallowequal "^1.1.0"
re-resizable@6.9.1:
version "6.9.1"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.1.tgz#6be082b55d02364ca4bfee139e04feebdf52441c"
integrity sha512-KRYAgr9/j1PJ3K+t+MBhlQ+qkkoLDJ1rs0z1heIWvYbCW/9Vq4djDU+QumJ3hQbwwtzXF6OInla6rOx6hhgRhQ==
dependencies:
fast-memoize "^2.5.1"
react-aspen@^1.1.0, react-aspen@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/react-aspen/-/react-aspen-1.1.1.tgz#61a85ef43748158322c4a3b73faaa5e563edd038"
@ -9063,6 +9067,14 @@ react-dom@^17.0.1:
object-assign "^4.1.1"
scheduler "^0.20.2"
react-draggable@4.4.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3"
integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-draggable@^4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f"
@ -9093,6 +9105,15 @@ react-property@1.0.1:
resolved "https://registry.yarnpkg.com/react-property/-/react-property-1.0.1.tgz#4ae4211557d0a0ae050a71aa8ad288c074bea4e6"
integrity sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ==
react-rnd@^10.3.5:
version "10.3.5"
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.3.5.tgz#b66e5e06f1eb6823e72eb4b552081b4b9241b139"
integrity sha512-LWJP+l5bp76sDPKrKM8pwGJifI6i3B5jHK4ONACczVMbR8ycNGA75ORRqpRuXGyKawUs68s1od05q8cqWgQXgw==
dependencies:
re-resizable "6.9.1"
react-draggable "4.4.3"
tslib "2.3.0"
react-select@^4.2.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81"
@ -10511,6 +10532,11 @@ trim-right@^1.0.1:
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslib@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"