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 :alt: Preferences dialog debugger keyboard shortcuts section
:align: center :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 The Miscellaneous Node
********************** **********************

View File

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

View File

@ -8,8 +8,8 @@
"license": "PostgreSQL", "license": "PostgreSQL",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.2", "@babel/core": "^7.10.2",
"@babel/eslint-parser": "^7.12.13", "@babel/eslint-parser": "^7.17.0",
"@babel/eslint-plugin": "^7.12.13", "@babel/eslint-plugin": "^7.17.7",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1", "@babel/plugin-proposal-object-rest-spread": "^7.10.1",
"@babel/plugin-syntax-jsx": "^7.16.0", "@babel/plugin-syntax-jsx": "^7.16.0",
"@babel/preset-env": "^7.10.2", "@babel/preset-env": "^7.10.2",
@ -145,7 +145,7 @@
"path-fx": "^2.0.0", "path-fx": "^2.0.0",
"pathfinding": "^0.4.18", "pathfinding": "^0.4.18",
"paths-js": "^0.4.9", "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", "postcss": "^8.2.15",
"raf": "^3.4.1", "raf": "^3.4.1",
"rc-dock": "^3.2.9", "rc-dock": "^3.2.9",
@ -154,6 +154,7 @@
"react-checkbox-tree": "^1.7.2", "react-checkbox-tree": "^1.7.2",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-draggable": "^4.4.4", "react-draggable": "^4.4.4",
"react-rnd": "^10.3.5",
"react-select": "^4.2.1", "react-select": "^4.2.1",
"react-table": "^7.6.3", "react-table": "^7.6.3",
"react-timer-hook": "^3.0.5", "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( self.open_in_new_tab = self.preference.register(
'tab_settings', 'new_browser_tab_open', '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, category_label=PREF_LABEL_OPTIONS,
options=ope_new_tab_options, options=ope_new_tab_options,
help_str=gettext( help_str=gettext(
@ -527,7 +527,7 @@ def register_browser_preferences(self):
'or PSQL Tool from the drop-down to set ' 'or PSQL Tool from the drop-down to set '
'open in new browser tab for that particular module.' 'open in new browser tab for that particular module.'
), ),
select2={ control_props={
'multiple': True, 'allowClear': False, 'multiple': True, 'allowClear': False,
'tags': True, 'first_empty': False, 'tags': True, 'first_empty': False,
'selectOnClose': False, 'emptyOptions': True, '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', 'user_language', 'user_language',
gettext("User language"), 'options', 'en', gettext("User language"), 'options', 'en',
category_label=gettext('User language'), category_label=gettext('User language'),
options=lang_options options=lang_options,
control_props={
'allowClear': False,
}
) )
theme_options = [] theme_options = []
@ -90,8 +93,11 @@ class MiscModule(PgAdminModule):
gettext("Theme"), 'options', 'standard', gettext("Theme"), 'options', 'standard',
category_label=gettext('Themes'), category_label=gettext('Themes'),
options=theme_options, options=theme_options,
control_props={
'allowClear': False,
},
help_str=gettext( 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' 'preview of the theme'
) )
) )

View File

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

View File

@ -37,26 +37,23 @@ class PreferencesModule(PgAdminModule):
""" """
def get_own_javascripts(self): def get_own_javascripts(self):
return [{ scripts = list()
'name': 'pgadmin.preferences', for name, script in [
'path': url_for('preferences.index') + 'preferences', ['pgadmin.preferences', 'js/preferences']
'when': None ]:
}] scripts.append({
'name': name,
'path': url_for('preferences.index') + script,
'when': None
})
return scripts
def get_own_stylesheets(self): def get_own_stylesheets(self):
return [] return []
def get_own_menuitems(self): def get_own_menuitems(self):
return { return {}
'file_items': [
MenuItem(name='mnu_preferences',
priority=997,
module="pgAdmin.Preferences",
callback='show',
icon='fa fa-cog',
label=gettext('Preferences'))
]
}
def get_exposed_url_endpoints(self): def get_exposed_url_endpoints(self):
""" """
@ -149,7 +146,8 @@ def _iterate_categories(pref_d, label, res):
"label": gettext(pref_d['label']), "label": gettext(pref_d['label']),
"inode": True, "inode": True,
"open": True, "open": True,
"branch": [] "children": [],
"value": gettext(pref_d['label']),
} }
for c in pref_d['categories']: for c in pref_d['categories']:
@ -162,13 +160,15 @@ def _iterate_categories(pref_d, label, res):
"id": c['id'], "id": c['id'],
"mid": pref_d['id'], "mid": pref_d['id'],
"label": gettext(c['label']), "label": gettext(c['label']),
"value": '{0}{1}'.format(c['id'], gettext(c['label'])),
"inode": False, "inode": False,
"open": False, "open": False,
"preferences": sorted(c['preferences'], key=label) "preferences": sorted(c['preferences'], key=label),
"showCheckbox": False
} }
(om['branch']).append(oc) (om['children']).append(oc)
om['branch'] = sorted(om['branch'], key=label) om['children'] = sorted(om['children'], key=label)
res.append(om) 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 @login_required
def save(pid): def save():
""" """
Save a specific preference. 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', for data in pref_data:
'qt_tab_title_placeholder', if data['name'] in ['vw_edt_tab_title_placeholder',
'debugger_tab_title_placeholder'] \ 'qt_tab_title_placeholder',
and data['value'].isspace(): 'debugger_tab_title_placeholder'] \
data['value'] = '' and data['value'].isspace():
data['value'] = ''
res, msg = Preferences.save( res, msg = Preferences.save(
data['mid'], data['category_id'], data['id'], data['value']) data['mid'], data['category_id'], data['id'], data['value'])
sgm.get_nodes(sgm) sgm.get_nodes(sgm)
if not res: if not res:
return internal_server_error(errormsg=msg) return internal_server_error(errormsg=msg)
response = success_return() response = success_return()
# Set cookie & session for language settings. # Set cookie & session for language settings.
# This will execute every time as could not find the better way to know # This will execute every time as could not find the better way to know
# that which preference is getting updated. # that which preference is getting updated.
misc_preference = Preferences.module('misc') misc_preference = Preferences.module('misc')
user_languages = misc_preference.preference( user_languages = misc_preference.preference(
'user_language' 'user_language'
) )
language = 'en' language = 'en'
if user_languages: if user_languages:
language = user_languages.get() or language language = user_languages.get() or language
domain = dict() domain = dict()
if config.COOKIE_DEFAULT_DOMAIN and\ if config.COOKIE_DEFAULT_DOMAIN and \
config.COOKIE_DEFAULT_DOMAIN != 'localhost': config.COOKIE_DEFAULT_DOMAIN != 'localhost':
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
setattr(session, 'PGADMIN_LANGUAGE', language) setattr(session, 'PGADMIN_LANGUAGE', language)
response.set_cookie("PGADMIN_LANGUAGE", value=language, response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH, path=config.COOKIE_DEFAULT_PATH,
secure=config.SESSION_COOKIE_SECURE, secure=config.SESSION_COOKIE_SECURE,
httponly=config.SESSION_COOKIE_HTTPONLY, httponly=config.SESSION_COOKIE_HTTPONLY,
samesite=config.SESSION_COOKIE_SAMESITE, samesite=config.SESSION_COOKIE_SAMESITE,
**domain) **domain)
return response 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 Notify from '../../../static/js/helpers/Notifier';
// import PreferencesTree from './components/PreferencesTree';
import { initPreferencesTree } from './components/PreferencesTree';
define('pgadmin.preferences', [ export default class Preferences {
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone', static instance;
'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.
/* static getInstance(...args) {
* Hmm... this module is already been initialized, we can refer to the old if (!Preferences.instance) {
* object from here. Preferences.instance = new Preferences(...args);
*/ }
if (pgAdmin.Preferences) return Preferences.instance;
return pgAdmin.Preferences; }
pgAdmin.Preferences = { constructor(pgAdmin, pgBrowser) {
init: function() { this.pgAdmin = pgAdmin;
if (this.initialized) this.pgBrowser = pgBrowser;
return; }
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 this.pgBrowser.add_menus(menus);
Alertify.dialog('preferencesDlg', function() { }
var jTree, // Variable to create the aci-tree // This is a callback function to show preferences.
controls = [], // Keep tracking of all the backform controls show() {
// created by the dialog. // Render Preferences component
// Dialog containter Notify.showModal(gettext('Preferences'), (closeModal) => {
$container = $('<div class=\'preferences_dialog d-flex flex-row\'></div>'); return <PreferencesComponent
renderTree={(prefTreeData) => {
initPreferencesTree(this.pgBrowser, document.getElementById('treeContainer'), prefTreeData);
/* }} closeModal={closeModal} />;
* Preference Model }, { isFullScreen: false, isResizeable: true, showFullScreen: true, isFullWidth: true, dialogWidth: 900, dialogHeight: 550 });
* }
* 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;
});

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

View File

@ -9,18 +9,19 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import {
import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor, FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, FormNote, FormInputDateTimePicker, PlainString, InputSQL, FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString,
InputSelect, InputText, InputCheckbox, InputDateTimePicker } from '../components/FormComponents'; InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputThemes, InputRadio
} from '../components/FormComponents';
import Privilege from '../components/Privilege'; import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils'; import { evalFunc } from 'sources/utils';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types'; import CustomPropTypes from '../custom_prop_types';
import { SelectRefresh} from '../components/SelectRefresh'; import { SelectRefresh } from '../components/SelectRefresh';
/* Control mapping for form view */ /* 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 name = id;
const onTextChange = useCallback((e) => { const onTextChange = useCallback((e) => {
let val = e; let val = e;
@ -34,36 +35,36 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
onChange && onChange(changedValue); onChange && onChange(changedValue);
}, []); }, []);
if(!visible) { if (!visible) {
return <></>; return <></>;
} }
/* The mapping uses Form* components as it comes with labels */ /* The mapping uses Form* components as it comes with labels */
switch (type) { switch (type) {
case 'int': 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': 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': 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': 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': case 'multiline':
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} return <FormInputText name={name} value={value} onChange={onTextChange} className={className}
inputRef={inputRef} controlProps={{multiline: true}} {...props}/>; inputRef={inputRef} controlProps={{ multiline: true }} {...props} />;
case 'password': 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': case 'select':
return <FormInputSelect name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />; return <FormInputSelect name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />;
case 'select-refresh': case 'select-refresh':
return <SelectRefresh name={name} value={value} onChange={onTextChange} className={className} {...props} />; return <SelectRefresh name={name} value={value} onChange={onTextChange} className={className} {...props} />;
case 'switch': case 'switch':
return <FormInputSwitch name={name} value={value} 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} />; {...props} />;
case 'checkbox': case 'checkbox':
return <FormInputCheckbox name={name} value={value} 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} />; {...props} />;
case 'toggle': case 'toggle':
return <FormInputToggle name={name} value={value} return <FormInputToggle name={name} value={value}
@ -76,9 +77,15 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
case 'sql': case 'sql':
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} {...props} />; return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} {...props} />;
case 'note': case 'note':
return <FormNote className={className} {...props}/>; return <FormNote className={className} {...props} />;
case 'datetimepicker': case 'datetimepicker':
return <FormInputDateTimePicker name={name} value={value} onChange={onTextChange} className={className} {...props} />; 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: default:
return <PlainString value={value} {...props} />; return <PlainString value={value} {...props} />;
} }
@ -100,17 +107,25 @@ MappedFormControlBase.propTypes = {
}; };
/* Control mapping for grid cell view */ /* 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 name = id;
const onTextChange = useCallback((e) => { const onTextChange = useCallback((e) => {
let val = e; let val = e;
if(e && e.target) { if (e && e.target) {
val = e.target.value; val = e.target.value;
} }
onCellChange && onCellChange(val); 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) => { const onSqlChange = useCallback((val) => {
onCellChange && onCellChange(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. /* Some grid cells are based on options selected in other cells.
* lets trigger a re-render for the row if optionsLoaded * 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 is called when select options are fetched */
optionsLoaded && optionsLoaded(res); optionsLoaded && optionsLoaded(res);
reRenderRow && reRenderRow(); reRenderRow && reRenderRow();
}, []); }, []);
if(!visible) { if (!visible) {
return <></>; return <></>;
} }
@ -152,6 +167,12 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>; return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
case 'sql': case 'sql':
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />; 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: default:
return <PlainString value={value} {...props} />; return <PlainString value={value} {...props} />;
} }
@ -167,14 +188,16 @@ MappedCellControlBase.propTypes = {
reRenderRow: PropTypes.func, reRenderRow: PropTypes.func,
optionsLoaded: PropTypes.func, optionsLoaded: PropTypes.func,
onCellChange: PropTypes.func, onCellChange: PropTypes.func,
visible: PropTypes.bool visible: PropTypes.bool,
disabled: PropTypes.bool,
inputRef: CustomPropTypes.ref,
}; };
const ALLOWED_PROPS_FIELD_COMMON = [ const ALLOWED_PROPS_FIELD_COMMON = [
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id', 'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef', 'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis', 'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
'orientation' 'orientation', 'isvalidate', 'fields', 'radioType'
]; ];
const ALLOWED_PROPS_FIELD_FORM = [ const ALLOWED_PROPS_FIELD_FORM = [
@ -182,14 +205,14 @@ const ALLOWED_PROPS_FIELD_FORM = [
]; ];
const ALLOWED_PROPS_FIELD_CELL = [ const ALLOWED_PROPS_FIELD_CELL = [
'cell', 'onCellChange', 'row', 'reRenderRow', 'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType'
]; ];
export const MappedFormControl = (props)=>{ export const MappedFormControl = (props) => {
let newProps = {...props}; let newProps = { ...props };
let typeProps = evalFunc(null, newProps.type, newProps.state); let typeProps = evalFunc(null, newProps.type, newProps.state);
if(typeof(typeProps) === 'object') { if (typeof (typeProps) === 'object') {
newProps = { newProps = {
...newProps, ...newProps,
...typeProps, ...typeProps,
@ -199,13 +222,13 @@ export const MappedFormControl = (props)=>{
} }
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */ /* 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)=>{ export const MappedCellControl = (props) => {
let newProps = {...props}; let newProps = { ...props };
let cellProps = evalFunc(null, newProps.cell, newProps.row); let cellProps = evalFunc(null, newProps.cell, newProps.row);
if(typeof(cellProps) === 'object') { if (typeof (cellProps) === 'object') {
newProps = { newProps = {
...newProps, ...newProps,
...cellProps, ...cellProps,
@ -215,5 +238,5 @@ export const MappedCellControl = (props)=>{
} }
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */ /* 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 */ /* The comparator and setter */
const attrChanged = (id, change, force=false)=>{ 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; return;
} else { } else {
change = change || _.get(sessVal, id); change = change || _.get(sessVal, id);
@ -302,6 +302,7 @@ export const SCHEMA_STATE_ACTIONS = {
RERENDER: 'rerender', RERENDER: 'rerender',
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue', CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
DEFERRED_DEPCHANGE: 'deferred_depchange', DEFERRED_DEPCHANGE: 'deferred_depchange',
BULK_UPDATE: 'bulk_update'
}; };
const getDepChange = (currPath, newState, oldState, action)=>{ const getDepChange = (currPath, newState, oldState, action)=>{
@ -354,6 +355,13 @@ const sessDataReducer = (state, action)=>{
case SCHEMA_STATE_ACTIONS.INIT: case SCHEMA_STATE_ACTIONS.INIT:
data = action.payload; data = action.payload;
break; 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: case SCHEMA_STATE_ACTIONS.SET_VALUE:
_.set(data, action.path, action.value); _.set(data, action.path, action.value);
/* If there is any dep listeners get the changes */ /* 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 RegexSvg from '../../img/fonticon/regex.svg?svgr';
import FormatCaseSvg from '../../img/fonticon/format_case.svg?svgr'; import FormatCaseSvg from '../../img/fonticon/format_case.svg?svgr';
import PropTypes from 'prop-types'; 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}) { export default function ExternalIcon({Icon, ...props}) {
return <Icon className='MuiSvgIcon-root' {...props} />; return <Icon className={'MuiSvgIcon-root'} {...props} />;
} }
ExternalIcon.propTypes = { 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 DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />; export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />; 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 React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { Box, FormControl, OutlinedInput, FormHelperText, import {
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect } from '@material-ui/core'; 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 { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
import ErrorRoundedIcon from '@material-ui/icons/ErrorOutlineRounded'; import ErrorRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
import InfoRoundedIcon from '@material-ui/icons/InfoRounded'; 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 WarningRoundedIcon from '@material-ui/icons/WarningRounded';
import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded'; import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded';
import DescriptionIcon from '@material-ui/icons/Description'; 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 CreatableSelect from 'react-select/creatable';
import Pickr from '@simonwep/pickr'; import Pickr from '@simonwep/pickr';
import clsx from 'clsx'; import clsx from 'clsx';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import HTMLReactParse from 'html-react-parser'; 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 DateFnsUtils from '@date-io/date-fns';
import * as DateFns from 'date-fns'; import * as DateFns from 'date-fns';
@ -36,6 +39,9 @@ import { showFileDialog } from '../helpers/legacyConnector';
import _ from 'lodash'; import _ from 'lodash';
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons'; import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
import CustomPropTypes from '../custom_prop_types'; import CustomPropTypes from '../custom_prop_types';
import KeyboardShortcuts from './KeyboardShortcuts';
import QueryThresholds from './QueryThresholds';
import Themes from './Themes';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -55,7 +61,7 @@ const useStyles = makeStyles((theme) => ({
margin: theme.spacing(0.75, 0.75, 0.75, 0.75), margin: theme.spacing(0.75, 0.75, 0.75, 0.75),
display: 'flex', display: 'flex',
}, },
formLabelError: { formLabelError: {
color: theme.palette.error.main, color: theme.palette.error.main,
}, },
sql: { sql: {
@ -95,17 +101,17 @@ export const MESSAGE_TYPE = {
}; };
/* Icon based on MESSAGE_TYPE */ /* Icon based on MESSAGE_TYPE */
function FormIcon({type, close=false, ...props}) { function FormIcon({ type, close = false, ...props }) {
let TheIcon = null; let TheIcon = null;
if(close) { if (close) {
TheIcon = CloseIcon; TheIcon = CloseIcon;
} else if(type === MESSAGE_TYPE.SUCCESS) { } else if (type === MESSAGE_TYPE.SUCCESS) {
TheIcon = CheckRoundedIcon; TheIcon = CheckRoundedIcon;
} else if(type === MESSAGE_TYPE.ERROR) { } else if (type === MESSAGE_TYPE.ERROR) {
TheIcon = ErrorRoundedIcon; TheIcon = ErrorRoundedIcon;
} else if(type === MESSAGE_TYPE.INFO) { } else if (type === MESSAGE_TYPE.INFO) {
TheIcon = InfoRoundedIcon; TheIcon = InfoRoundedIcon;
} else if(type === MESSAGE_TYPE.WARNING) { } else if (type === MESSAGE_TYPE.WARNING) {
TheIcon = WarningRoundedIcon; TheIcon = WarningRoundedIcon;
} }
@ -117,21 +123,21 @@ FormIcon.propTypes = {
}; };
/* Wrapper on any form component to add label, error indicator and help message */ /* 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 classes = useStyles();
const cid = testcid || _.uniqueId('c'); const cid = testcid || _.uniqueId('c');
const helpid = `h${cid}`; const helpid = `h${cid}`;
return ( return (
<Grid container spacing={0} className={className}> <Grid container spacing={0} className={className}>
<Grid item lg={3} md={3} sm={3} xs={12}> <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} {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> </InputLabel>
</Grid> </Grid>
<Grid item lg={9} md={9} sm={9} xs={12}> <Grid item lg={9} md={9} sm={9} xs={12}>
<FormControl error={Boolean(error)} fullWidth> <FormControl error={Boolean(error)} fullWidth>
{React.cloneElement(children, {cid, helpid})} {React.cloneElement(children, { cid, helpid })}
</FormControl> </FormControl>
<FormHelperText id={helpid} variant="outlined">{HTMLReactParse(helpMessage || '')}</FormHelperText> <FormHelperText id={helpid} variant="outlined">{HTMLReactParse(helpMessage || '')}</FormHelperText>
</Grid> </Grid>
@ -148,17 +154,22 @@ FormInput.propTypes = {
testcid: PropTypes.any, testcid: PropTypes.any,
}; };
export function InputSQL({value, onChange, className, controlProps, ...props}) { export function InputSQL({ value, options, onChange, className, controlProps, ...props }) {
const classes = useStyles(); const classes = useStyles();
const editor = useRef(); const editor = useRef();
return ( return (
<CodeMirror <CodeMirror
currEditor={(obj)=>editor.current=obj} currEditor={(obj) => editor.current = obj}
value={value||''} value={value || ''}
options={{
lineNumbers: true,
mode: 'text/x-pgsql',
...options,
}}
className={clsx(classes.sql, className)} className={clsx(classes.sql, className)}
events={{ events={{
change: (cm)=>{ change: (cm) => {
onChange && onChange(cm.getValue()); onChange && onChange(cm.getValue());
}, },
}} }}
@ -176,13 +187,13 @@ InputSQL.propTypes = {
controlProps: PropTypes.object, controlProps: PropTypes.object,
}; };
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, noLabel, ...props}) { export function FormInputSQL({ hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props }) {
if(noLabel) { if (noLabel) {
return <InputSQL value={value} {...props}/>; return <InputSQL value={value} options={controlProps} {...props} />;
} else { } else {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} > <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} {...props}/> <InputSQL value={value} options={controlProps} {...props} />
</FormInput> </FormInput>
); );
} }
@ -208,7 +219,7 @@ const DATE_TIME_FORMAT = {
TIME_24: 'HH:mm:ss', TIME_24: 'HH:mm:ss',
}; };
export function InputDateTimePicker({value, onChange, readonly, controlProps, ...props}) { export function InputDateTimePicker({ value, onChange, readonly, controlProps, ...props }) {
let format = ''; let format = '';
let placeholder = ''; let placeholder = '';
if (controlProps?.pickerType === 'Date') { 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'; placeholder = controlProps.placeholder || 'YYYY-MM-DD HH:mm:ss Z';
} }
const handleChange = (dateVal, stringVal)=> { const handleChange = (dateVal, stringVal) => {
onChange(stringVal); onChange(stringVal);
}; };
/* Value should be a date object instead of string */ /* Value should be a date object instead of string */
value = _.isUndefined(value) ? null : value; value = _.isUndefined(value) ? null : value;
if(!_.isNull(value)) { if (!_.isNull(value)) {
let parseValue = DateFns.parse(value, format, new Date()); let parseValue = DateFns.parse(value, format, new Date());
if(!DateFns.isValid(parseValue)) { if (!DateFns.isValid(parseValue)) {
parseValue = DateFns.parseISO(value); parseValue = DateFns.parseISO(value);
} }
value = !DateFns.isValid(parseValue) ? value : parseValue; value = !DateFns.isValid(parseValue) ? value : parseValue;
@ -238,7 +249,7 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
if (readonly) { if (readonly) {
return (<InputText value={value ? DateFns.format(value, format) : value} 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 = { let commonProps = {
@ -262,20 +273,20 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
if (controlProps?.pickerType === 'Date') { if (controlProps?.pickerType === 'Date') {
return ( return (
<MuiPickersUtilsProvider utils={DateFnsUtils}> <MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker {...commonProps}/> <KeyboardDatePicker {...commonProps} />
</MuiPickersUtilsProvider> </MuiPickersUtilsProvider>
); );
} else if (controlProps?.pickerType === 'Time') { } else if (controlProps?.pickerType === 'Time') {
return ( return (
<MuiPickersUtilsProvider utils={DateFnsUtils}> <MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardTimePicker {...commonProps}/> <KeyboardTimePicker {...commonProps} />
</MuiPickersUtilsProvider> </MuiPickersUtilsProvider>
); );
} }
return ( return (
<MuiPickersUtilsProvider utils={DateFnsUtils}> <MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDateTimePicker {...commonProps}/> <KeyboardDateTimePicker {...commonProps} />
</MuiPickersUtilsProvider> </MuiPickersUtilsProvider>
); );
} }
@ -287,10 +298,10 @@ InputDateTimePicker.propTypes = {
controlProps: PropTypes.object, controlProps: PropTypes.object,
}; };
export function FormInputDateTimePicker({hasError, required, label, className, helpMessage, testcid, ...props}) { export function FormInputDateTimePicker({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputDateTimePicker {...props}/> <InputDateTimePicker {...props} />
</FormInput> </FormInput>
); );
} }
@ -308,23 +319,23 @@ FormInputDateTimePicker.propTypes = {
/* Use forwardRef to pass ref prop to OutlinedInput */ /* Use forwardRef to pass ref prop to OutlinedInput */
export const InputText = forwardRef(({ 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 classes = useStyles();
const patterns = { const patterns = {
'numeric': '^-?[0-9]\\d*\\.?\\d*$', 'numeric': '^-?[0-9]\\d*\\.?\\d*$',
'int': '^-?[0-9]\\d*$', 'int': '^-?[0-9]\\d*$',
}; };
let onChangeFinal = (e)=>{ let onChangeFinal = (e) => {
let changeVal = e.target.value; let changeVal = e.target.value;
/* For type number, we set type as tel with number regex to get validity.*/ /* For type number, we set type as tel with number regex to get validity.*/
if(['numeric', 'int', 'tel'].indexOf(type) > -1) { if (['numeric', 'int', 'tel'].indexOf(type) > -1) {
if(!e.target.validity.valid && changeVal !== '' && changeVal !== '-') { if (!e.target.validity.valid && changeVal !== '' && changeVal !== '-') {
return; return;
} }
} }
if(controlProps?.formatter) { if (controlProps?.formatter) {
changeVal = controlProps.formatter.toRaw(changeVal); changeVal = controlProps.formatter.toRaw(changeVal);
} }
onChange && onChange(changeVal); onChange && onChange(changeVal);
@ -332,11 +343,11 @@ export const InputText = forwardRef(({
let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value; let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;
if(controlProps?.formatter) { if (controlProps?.formatter) {
finalValue = controlProps.formatter.fromRaw(finalValue); finalValue = controlProps.formatter.fromRaw(finalValue);
} }
return( return (
<OutlinedInput <OutlinedInput
ref={ref} ref={ref}
color="primary" color="primary"
@ -346,7 +357,7 @@ export const InputText = forwardRef(({
id: cid, id: cid,
maxLength: controlProps?.multiline ? null : maxlength, maxLength: controlProps?.multiline ? null : maxlength,
'aria-describedby': helpid, '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)} readOnly={Boolean(readonly)}
disabled={Boolean(disabled)} disabled={Boolean(disabled)}
@ -354,9 +365,12 @@ export const InputText = forwardRef(({
notched={false} notched={false}
value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue} value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue}
onChange={onChangeFinal} onChange={onChangeFinal}
{
...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown })
}
{...controlProps} {...controlProps}
{...props} {...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, type: PropTypes.string,
}; };
export function FormInputText({hasError, required, label, className, helpMessage, testcid, ...props}) { export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputText label={label} {...props}/> <InputText label={label} {...props} />
</FormInput> </FormInput>
); );
} }
@ -391,16 +405,21 @@ FormInputText.propTypes = {
}; };
/* Using the existing file dialog functions using showFileDialog */ /* 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 inpRef = useRef();
const onFileSelect = (value)=>{ const onFileSelect = (value) => {
onChange && onChange(decodeURI(value)); onChange && onChange(decodeURI(value));
inpRef.current.focus(); inpRef.current.focus();
}; };
return ( return (
<InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} {...props} endAdornment={ <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, onChange: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
readonly: PropTypes.bool, readonly: PropTypes.bool,
isvalidate: PropTypes.bool,
validate: PropTypes.func,
value: PropTypes.string
}; };
export function FormInputFileSelect({ export function FormInputFileSelect({
hasError, required, label, className, helpMessage, testcid, ...props}) { hasError, required, label, className, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <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> </FormInput>
); );
} }
@ -429,13 +451,13 @@ FormInputFileSelect.propTypes = {
testcid: PropTypes.string, 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(); const classes = useStyles();
return ( return (
<Switch color="primary" <Switch color="primary"
checked={Boolean(value)} checked={Boolean(value)}
onChange={ onChange={
readonly ? ()=>{/*This is intentional (SonarQube)*/} : onChange readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange
} }
id={cid} id={cid}
inputProps={{ inputProps={{
@ -457,11 +479,11 @@ InputSwitch.propTypes = {
controlProps: PropTypes.object, controlProps: PropTypes.object,
}; };
export function FormInputSwitch({hasError, required, label, className, helpMessage, testcid, ...props}) { export function FormInputSwitch({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputSwitch {...props}/> <InputSwitch {...props} />
</FormInput> </FormInput>
); );
} }
@ -474,7 +496,7 @@ FormInputSwitch.propTypes = {
testcid: PropTypes.string, 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 || {}; controlProps = controlProps || {};
return ( return (
<FormControlLabel <FormControlLabel
@ -482,12 +504,13 @@ export function InputCheckbox({cid, helpid, value, onChange, controlProps, reado
<Checkbox <Checkbox
id={cid} id={cid}
checked={Boolean(value)} checked={Boolean(value)}
onChange={readonly ? ()=>{/*This is intentional (SonarQube)*/} : onChange} onChange={readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange}
color="primary" color="primary"
inputProps={{'aria-describedby': helpid}} inputProps={{ 'aria-describedby': helpid }}
{...props}/> {...props} />
} }
label={controlProps.label} label={controlProps.label}
labelPlacement={props?.labelPlacement ? props.labelPlacement : 'end'}
/> />
); );
} }
@ -498,14 +521,15 @@ InputCheckbox.propTypes = {
controlProps: PropTypes.object, controlProps: PropTypes.object,
onChange: PropTypes.func, onChange: PropTypes.func,
readonly: PropTypes.bool, readonly: PropTypes.bool,
labelPlacement: PropTypes.string
}; };
export function FormInputCheckbox({hasError, required, label, export function FormInputCheckbox({ hasError, required, label,
className, helpMessage, testcid, ...props}) { className, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputCheckbox {...props}/> <InputCheckbox {...props} />
</FormInput> </FormInput>
); );
} }
@ -518,24 +542,61 @@ FormInputCheckbox.propTypes = {
testcid: PropTypes.string, 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 ( return (
<ToggleButtonGroup <ToggleButtonGroup
id={cid} id={cid}
value={value} value={value}
exclusive exclusive
onChange={(e, val)=>{val!==null && onChange(val);}} onChange={(e, val) => { val !== null && onChange(val); }}
{...props} {...props}
> >
{ {
(options||[]).map((option, i)=>{ (options || []).map((option, i) => {
const isSelected = option.value === value; const isSelected = option.value === value;
const isDisabled = disabled || option.disabled || (readonly && !isSelected); const isDisabled = disabled || option.disabled || (readonly && !isSelected);
return ( 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}> 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> </ToggleButton>
); );
}) })
@ -554,11 +615,11 @@ InputToggle.propTypes = {
readonly: PropTypes.bool, readonly: PropTypes.bool,
}; };
export function FormInputToggle({hasError, required, label, export function FormInputToggle({ hasError, required, label,
className, helpMessage, testcid, inputRef, ...props}) { className, helpMessage, testcid, inputRef, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputToggle ref={inputRef} {...props}/> <InputToggle ref={inputRef} {...props} />
</FormInput> </FormInput>
); );
} }
@ -575,9 +636,9 @@ FormInputToggle.propTypes = {
/* react-select package is used for select input /* react-select package is used for select input
* Customizing the select styles to fit existing theme * Customizing the select styles to fit existing theme
*/ */
const customReactSelectStyles = (theme, readonly)=>({ const customReactSelectStyles = (theme, readonly) => ({
input: (provided) => { input: (provided) => {
return {...provided, padding: 0, margin: 0, color: 'inherit'}; return { ...provided, padding: 0, margin: 0, color: 'inherit' };
}, },
singleValue: (provided) => { singleValue: (provided) => {
return { return {
@ -593,35 +654,35 @@ const customReactSelectStyles = (theme, readonly)=>({
borderColor: theme.otherVars.inputBorderColor, borderColor: theme.otherVars.inputBorderColor,
...(state.isFocused ? { ...(state.isFocused ? {
borderColor: theme.palette.primary.main, 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': { '&:hover': {
borderColor: theme.palette.primary.main, borderColor: theme.palette.primary.main,
} }
} : {}), } : {}),
}), }),
dropdownIndicator: (provided)=>({ dropdownIndicator: (provided) => ({
...provided, ...provided,
padding: '0rem 0.25rem', padding: '0rem 0.25rem',
}), }),
indicatorsContainer: (provided)=>({ indicatorsContainer: (provided) => ({
...provided, ...provided,
...(readonly ? {display: 'none'} : {}) ...(readonly ? { display: 'none' } : {})
}), }),
clearIndicator: (provided)=>({ clearIndicator: (provided) => ({
...provided, ...provided,
padding: '0rem 0.25rem', padding: '0rem 0.25rem',
}), }),
valueContainer: (provided)=>({ valueContainer: (provided) => ({
...provided, ...provided,
padding: theme.otherVars.reactSelect.padding, padding: theme.otherVars.reactSelect.padding,
}), }),
groupHeading: (provided)=>({ groupHeading: (provided) => ({
...provided, ...provided,
color: 'inherit', color: 'inherit',
fontSize: '0.85em', fontSize: '0.85em',
textTransform: 'none', textTransform: 'none',
}), }),
menu: (provided)=>({ menu: (provided) => ({
...provided, ...provided,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
@ -629,12 +690,12 @@ const customReactSelectStyles = (theme, readonly)=>({
border: '1px solid ' + theme.otherVars.inputBorderColor, border: '1px solid ' + theme.otherVars.inputBorderColor,
marginTop: '2px', marginTop: '2px',
}), }),
menuPortal: (provided)=>({ menuPortal: (provided) => ({
...provided, zIndex: 9999, ...provided, zIndex: 9999,
backgroundColor: 'inherit', backgroundColor: 'inherit',
color: 'inherit', color: 'inherit',
}), }),
option: (provided, state)=>{ option: (provided, state) => {
let bgColor = 'inherit'; let bgColor = 'inherit';
if (state.isFocused) { if (state.isFocused) {
bgColor = theme.palette.grey[400]; bgColor = theme.palette.grey[400];
@ -648,27 +709,27 @@ const customReactSelectStyles = (theme, readonly)=>({
backgroundColor: bgColor, backgroundColor: bgColor,
}; };
}, },
multiValue: (provided)=>({ multiValue: (provided) => ({
...provided, ...provided,
backgroundColor: theme.palette.grey[400], backgroundColor: theme.palette.grey[400],
}), }),
multiValueLabel: (provided)=>({ multiValueLabel: (provided) => ({
...provided, ...provided,
fontSize: '1em', fontSize: '1em',
zIndex: 99, zIndex: 99,
color: theme.palette.text.primary color: theme.palette.text.primary
}), }),
multiValueRemove: (provided)=>({ multiValueRemove: (provided) => ({
...provided, ...provided,
'&:hover': { '&:hover': {
backgroundColor: 'unset', backgroundColor: 'unset',
color: theme.palette.error.main, color: theme.palette.error.main,
}, },
...(readonly ? {display: 'none'} : {}) ...(readonly ? { display: 'none' } : {})
}), }),
}); });
function OptionView({image, label}) { function OptionView({ image, label }) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<> <>
@ -705,8 +766,8 @@ CustomSelectSingleValue.propTypes = {
}; };
export function flattenSelectOptions(options) { export function flattenSelectOptions(options) {
return _.flatMap(options, (option)=>{ return _.flatMap(options, (option) => {
if(option.options) { if (option.options) {
return option.options; return option.options;
} else { } else {
return option; return option;
@ -716,28 +777,28 @@ export function flattenSelectOptions(options) {
function getRealValue(options, value, creatable, formatter) { function getRealValue(options, value, creatable, formatter) {
let realValue = null; let realValue = null;
if(_.isArray(value)) { if (_.isArray(value)) {
realValue = [...value]; realValue = [...value];
/* If multi select options need to be in some format by UI, use formatter */ /* If multi select options need to be in some format by UI, use formatter */
if(formatter) { if (formatter) {
realValue = formatter.fromRaw(realValue, options); realValue = formatter.fromRaw(realValue, options);
} else { } else {
if(creatable) { if (creatable) {
realValue = realValue.map((val)=>({label:val, value: val})); realValue = realValue.map((val) => ({ label: val, value: val }));
} else { } else {
realValue = realValue.map((val)=>(_.find(options, (option)=>_.isEqual(option.value, val)))); realValue = realValue.map((val) => (_.find(options, (option) => _.isEqual(option.value, val))));
} }
} }
} else { } else {
let flatOptions = flattenSelectOptions(options); let flatOptions = flattenSelectOptions(options);
realValue = _.find(flatOptions, (option)=>option.value==value) || realValue = _.find(flatOptions, (option) => option.value == value) ||
(creatable && !_.isUndefined(value) && !_.isNull(value) ? {label:value, value: value} : null); (creatable && !_.isUndefined(value) && !_.isNull(value) ? { label: value, value: value } : null);
} }
return realValue; return realValue;
} }
export function InputSelectNonSearch({options, ...props}) { export function InputSelectNonSearch({ options, ...props }) {
return <MuiSelect native {...props} variant="outlined"> 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>; </MuiSelect>;
} }
InputSelectNonSearch.propTypes = { InputSelectNonSearch.propTypes = {
@ -748,7 +809,7 @@ InputSelectNonSearch.propTypes = {
}; };
export const InputSelect = forwardRef(({ 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 [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
const theme = useTheme(); const theme = useTheme();
@ -757,33 +818,33 @@ export const InputSelect = forwardRef(({
loading the options. optionsReloadBasis is helpful to avoid repeated loading the options. optionsReloadBasis is helpful to avoid repeated
options load. If optionsReloadBasis value changes, then options will be loaded again. options load. If optionsReloadBasis value changes, then options will be loaded again.
*/ */
useEffect(()=>{ useEffect(() => {
let optPromise = options, umounted=false; let optPromise = options, umounted = false;
if(typeof options === 'function') { if (typeof options === 'function') {
optPromise = options(); optPromise = options();
} }
setFinalOptions([[], true]); setFinalOptions([[], true]);
Promise.resolve(optPromise) Promise.resolve(optPromise)
.then((res)=>{ .then((res) => {
/* If component unmounted, dont update state */ /* If component unmounted, dont update state */
if(!umounted) { if (!umounted) {
optionsLoaded && optionsLoaded(res, value); optionsLoaded && optionsLoaded(res, value);
/* Auto select if any option has key as selected */ /* Auto select if any option has key as selected */
const flatRes = flattenSelectOptions(res || []); const flatRes = flattenSelectOptions(res || []);
let selectedVal; let selectedVal;
if(controlProps.multiple) { if (controlProps.multiple) {
selectedVal = _.filter(flatRes, (o)=>o.selected)?.map((o)=>o.value); selectedVal = _.filter(flatRes, (o) => o.selected)?.map((o) => o.value);
} else { } 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); onChange && onChange(selectedVal);
} }
setFinalOptions([res || [], false]); setFinalOptions([res || [], false]);
} }
}); });
return ()=>umounted=true; return () => umounted = true;
}, [optionsReloadBasis]); }, [optionsReloadBasis]);
@ -791,7 +852,7 @@ export const InputSelect = forwardRef(({
const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions; const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions;
const flatFiltered = flattenSelectOptions(filteredOptions); const flatFiltered = flattenSelectOptions(filteredOptions);
let realValue = getRealValue(flatFiltered, value, controlProps.creatable, controlProps.formatter); 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); console.error('Undefined option value not allowed', realValue, filteredOptions);
} }
const otherProps = { const otherProps = {
@ -802,17 +863,17 @@ export const InputSelect = forwardRef(({
const styles = customReactSelectStyles(theme, readonly || disabled); const styles = customReactSelectStyles(theme, readonly || disabled);
const onChangeOption = useCallback((selectVal)=>{ const onChangeOption = useCallback((selectVal) => {
if(_.isArray(selectVal)) { if (_.isArray(selectVal)) {
// Check if select all option is selected // Check if select all option is selected
if (!_.isUndefined(selectVal.find(x => x.label === 'Select All'))) { if (!_.isUndefined(selectVal.find(x => x.label === 'Select All'))) {
selectVal = filteredOptions; selectVal = filteredOptions;
} }
/* If multi select options need to be in some format by UI, use formatter */ /* 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); selectVal = controlProps.formatter.toRaw(selectVal, filteredOptions);
} else { } else {
selectVal = selectVal.map((option)=>option.value); selectVal = selectVal.map((option) => option.value);
} }
onChange && onChange(selectVal); onChange && onChange(selectVal);
} else { } else {
@ -838,13 +899,13 @@ export const InputSelect = forwardRef(({
...otherProps, ...otherProps,
...props, ...props,
}; };
if(!controlProps.creatable) { if (!controlProps.creatable) {
return ( return (
<Select ref={ref} {...commonProps}/> <Select ref={ref} {...commonProps} />
); );
} else { } else {
return ( return (
<CreatableSelect ref={ref} {...commonProps}/> <CreatableSelect ref={ref} {...commonProps} />
); );
} }
}); });
@ -863,10 +924,10 @@ InputSelect.propTypes = {
export function FormInputSelect({ export function FormInputSelect({
hasError, required, className, label, helpMessage, testcid, ...props}) { hasError, required, className, label, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputSelect ref={props.inputRef} {...props}/> <InputSelect ref={props.inputRef} {...props} />
</FormInput> </FormInput>
); );
} }
@ -881,7 +942,7 @@ FormInputSelect.propTypes = {
}; };
/* React wrapper on color pickr */ /* React wrapper on color pickr */
export function InputColor({value, controlProps, disabled, onChange, currObj}) { export function InputColor({ value, controlProps, disabled, onChange, currObj }) {
const pickrOptions = { const pickrOptions = {
showPalette: true, showPalette: true,
allowEmpty: true, allowEmpty: true,
@ -896,19 +957,19 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
const pickrObj = useRef(); const pickrObj = useRef();
const classes = useStyles(); const classes = useStyles();
const setColor = (newVal)=>{ const setColor = (newVal) => {
pickrObj.current && pickrObj.current &&
pickrObj.current.setColor((_.isUndefined(newVal) || newVal == '') ? pickrOptions.defaultColor : newVal); pickrObj.current.setColor((_.isUndefined(newVal) || newVal == '') ? pickrOptions.defaultColor : newVal);
}; };
const destroyPickr = ()=>{ const destroyPickr = () => {
if(pickrObj.current) { if (pickrObj.current) {
pickrObj.current.destroy(); pickrObj.current.destroy();
pickrObj.current = null; pickrObj.current = null;
} }
}; };
const initPickr = ()=>{ const initPickr = () => {
/* pickr does not have way to update options, need to /* pickr does not have way to update options, need to
destroy and recreate pickr to reflect options */ destroy and recreate pickr to reflect options */
destroyPickr(); destroyPickr();
@ -920,7 +981,7 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
swatches: [ swatches: [
'#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0', '#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0',
'#f0f', '#f4cccc', '#fce5cd', '#d0e0e3', '#cfe2f3', '#ead1dc', '#ea9999', '#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', '#f1c232', '#6aa84f', '#45818e', '#a64d79', '#bf9000', '#0c343d', '#4c1130',
], ],
position: pickrOptions.position, position: pickrOptions.position,
@ -941,20 +1002,20 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
setColor(value); setColor(value);
disabled && instance.disable(); disabled && instance.disable();
const {lastColor} = instance.getRoot().preview; const { lastColor } = instance.getRoot().preview;
const {clear} = instance.getRoot().interaction; const { clear } = instance.getRoot().interaction;
/* Cycle the keyboard navigation within the color picker */ /* Cycle the keyboard navigation within the color picker */
clear.addEventListener('keydown', (e)=>{ clear.addEventListener('keydown', (e) => {
if(e.keyCode === 9) { if (e.keyCode === 9) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
lastColor.focus(); lastColor.focus();
} }
}); });
lastColor.addEventListener('keydown', (e)=>{ lastColor.addEventListener('keydown', (e) => {
if(e.keyCode === 9 && e.shiftKey) { if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
clear.focus(); clear.focus();
@ -965,32 +1026,32 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
}).on('change', (color) => { }).on('change', (color) => {
onChange && onChange(color.toHEXA().toString()); onChange && onChange(color.toHEXA().toString());
}).on('show', (color, instance) => { }).on('show', (color, instance) => {
const {palette} = instance.getRoot().palette; const { palette } = instance.getRoot().palette;
palette.focus(); palette.focus();
}).on('hide', (instance) => { }).on('hide', (instance) => {
const button = instance.getRoot().button; const button = instance.getRoot().button;
button.focus(); button.focus();
}); });
if(currObj) { if (currObj) {
currObj(pickrObj.current); currObj(pickrObj.current);
} }
}; };
useEffect(()=>{ useEffect(() => {
initPickr(); initPickr();
return ()=>{ return () => {
destroyPickr(); destroyPickr();
}; };
}, [...Object.values(pickrOptions)]); }, [...Object.values(pickrOptions)]);
useEffect(()=>{ useEffect(() => {
if(pickrObj.current) { if (pickrObj.current) {
setColor(value); setColor(value);
} }
}, [value]); }, [value]);
let btnStyles = {backgroundColor: value}; let btnStyles = { backgroundColor: value };
return ( return (
<PgIconButton ref={eleRef} title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled} <PgIconButton ref={eleRef} title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}
icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />} icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
@ -1006,11 +1067,11 @@ InputColor.propTypes = {
}; };
export function FormInputColor({ export function FormInputColor({
hasError, required, className, label, helpMessage, testcid, ...props}) { hasError, required, className, label, helpMessage, testcid, ...props }) {
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}> <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputColor {...props}/> <InputColor {...props} />
</FormInput> </FormInput>
); );
} }
@ -1023,9 +1084,9 @@ FormInputColor.propTypes = {
testcid: PropTypes.string, testcid: PropTypes.string,
}; };
export function PlainString({controlProps, value}) { export function PlainString({ controlProps, value }) {
let finalValue = value; let finalValue = value;
if(controlProps?.formatter) { if (controlProps?.formatter) {
finalValue = controlProps.formatter.fromRaw(finalValue); finalValue = controlProps.formatter.fromRaw(finalValue);
} }
return <span>{finalValue}</span>; return <span>{finalValue}</span>;
@ -1035,7 +1096,7 @@ PlainString.propTypes = {
value: PropTypes.any, value: PropTypes.any,
}; };
export function FormNote({text, className}) { export function FormNote({ text, className }) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<Box className={className}> <Box className={className}>
@ -1051,7 +1112,7 @@ FormNote.propTypes = {
className: CustomPropTypes.className, className: CustomPropTypes.className,
}; };
const useStylesFormFooter = makeStyles((theme)=>({ const useStylesFormFooter = makeStyles((theme) => ({
root: { root: {
padding: theme.spacing(0.5), padding: theme.spacing(0.5),
position: 'absolute', position: 'absolute',
@ -1108,7 +1169,7 @@ const useStylesFormFooter = makeStyles((theme)=>({
export function FormFooterMessage(props) { export function FormFooterMessage(props) {
const classes = useStylesFormFooter(); const classes = useStylesFormFooter();
if(!props.message) { if (!props.message) {
return <></>; return <></>;
} }
return ( return (
@ -1122,15 +1183,81 @@ FormFooterMessage.propTypes = {
message: PropTypes.string, 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(); const classes = useStylesFormFooter();
return ( return (
<Box className={clsx(classes.container, classes[`container${type}`])}> <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> <Box className={classes.message}>{message}</Box>
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}> {closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
<FormIcon close={true}/> <FormIcon close={true} />
</IconButton>} </IconButton>}
</Box> </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 { Box, Dialog, DialogContent, DialogTitle, makeStyles, Paper } from '@material-ui/core';
import React from 'react'; import React, { useState } from 'react';
import {getEpoch} from 'sources/utils'; import { getEpoch } from 'sources/utils';
import { DefaultButton, PgIconButton, PrimaryButton } from '../components/Buttons'; import { DefaultButton, PgIconButton, PrimaryButton } from '../components/Buttons';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import CloseIcon from '@material-ui/icons/CloseRounded'; import CloseIcon from '@material-ui/icons/CloseRounded';
@ -19,13 +19,15 @@ import gettext from 'sources/gettext';
import Theme from '../Theme'; import Theme from '../Theme';
import HTMLReactParser from 'html-react-parser'; import HTMLReactParser from 'html-react-parser';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { Rnd } from 'react-rnd';
import { ExpandDialog, MinimizeDialog } from '../components/ExternalIcon';
const ModalContext = React.createContext({}); const ModalContext = React.createContext({});
export function useModal() { export function useModal() {
return React.useContext(ModalContext); return React.useContext(ModalContext);
} }
const useAlertStyles = makeStyles((theme)=>({ const useAlertStyles = makeStyles((theme) => ({
footer: { footer: {
display: 'flex', display: 'flex',
justifyContent: 'flex-end', 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(); const classes = useAlertStyles();
return ( return (
<Box display="flex" flexDirection="column" height="100%"> <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}> <Box className={classes.footer}>
{confirm && {confirm &&
<DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} >{cancelLabel}</DefaultButton> <DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} >{cancelLabel}</DefaultButton>
@ -60,10 +62,10 @@ AlertContent.propTypes = {
cancelLabel: PropTypes.string, 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 // bind the modal provider before calling
this.showModal(title, (closeModal)=>{ this.showModal(title, (closeModal) => {
const onOkClickClose = ()=>{ const onOkClickClose = () => {
onOkClick && onOkClick(); onOkClick && onOkClick();
closeModal(); 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 // bind the modal provider before calling
this.showModal(title, (closeModal)=>{ this.showModal(title, (closeModal) => {
const onCancelClickClose = ()=>{ const onCancelClickClose = () => {
onCancelClick && onCancelClick(); onCancelClick && onCancelClick();
closeModal(); closeModal();
}; };
const onOkClickClose = ()=>{ const onOkClickClose = () => {
onOkClick && onOkClick(); onOkClick && onOkClick();
closeModal(); closeModal();
}; };
return ( 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 [modals, setModals] = React.useState([]);
const showModal = (title, content, modalOptions)=>{ const showModal = (title, content, modalOptions) => {
let id = getEpoch().toString() + Math.random(); let id = getEpoch().toString() + Math.random();
setModals((prev)=>[...prev, { setModals((prev) => [...prev, {
id: id, id: id,
title: title, title: title,
content: content, content: content,
...modalOptions, ...modalOptions,
}]); }]);
}; };
const closeModal = (id)=>{ const closeModal = (id) => {
setModals((prev)=>{ setModals((prev) => {
return prev.filter((o)=>o.id!=id); return prev.filter((o) => o.id != id);
}); });
}; };
const fullScreenModal = (fullScreen) => {
setModals((prev) => [...prev, {
fullScreen: fullScreen,
}]);
};
const modalContextBase = { const modalContextBase = {
showModal: showModal, showModal: showModal,
closeModal: closeModal, closeModal: closeModal,
fullScreenModal: fullScreenModal
}; };
const modalContext = React.useMemo(()=>({ const modalContext = React.useMemo(() => ({
...modalContextBase, ...modalContextBase,
confirm: confirm.bind(modalContextBase), confirm: confirm.bind(modalContextBase),
alert: alert.bind(modalContextBase) alert: alert.bind(modalContextBase)
@ -119,8 +129,8 @@ export default function ModalProvider({children}) {
return ( return (
<ModalContext.Provider value={modalContext}> <ModalContext.Provider value={modalContext}>
{children} {children}
{modals.map((modalOptions, i)=>( {modals.map((modalOptions, i) => (
<ModalContainer key={i} {...modalOptions}/> <ModalContainer key={i} {...modalOptions} />
))} ))}
</ModalContext.Provider> </ModalContext.Provider>
); );
@ -130,30 +140,118 @@ ModalProvider.propTypes = {
children: CustomPropTypes.children, 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) { function PaperComponent(props) {
let classes = dialogStyle();
let [dialogPosition, setDialogPosition] = useState(null);
let resizeable = props.isresizeable == 'true' ? true : false;
return ( return (
<Draggable cancel={'[class*="MuiDialogContent-root"]'}> props.isresizeable == 'true' ?
<Paper {...props} style={{minWidth: '600px'}} /> <Rnd
</Draggable> 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 useModalRef = useModal();
let closeModal = ()=>useModalRef.closeModal(id); const classes = useModalStyles();
let closeModal = () => useModalRef.closeModal(id);
const [isfullScreen, setIsFullScreen] = useState(fullScreen);
return ( return (
<Theme> <Theme>
<Dialog <Dialog
open={true} open={true}
onClose={closeModal} onClose={closeModal}
PaperComponent={PaperComponent} PaperComponent={PaperComponent}
PaperProps={{ 'isfullscreen': isfullScreen.toString(), 'isresizeable': isResizeable.toString(), width: dialogWidth, height: dialogHeight }}
fullScreen={isfullScreen}
fullWidth={isFullWidth}
disableBackdropClick disableBackdropClick
> >
<DialogTitle> <DialogTitle>
<Box marginRight="0.25rem">{title}</Box> <Box className={classes.titleBar}>
<Box marginLeft="auto"><PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={closeModal}/></Box> <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> </DialogTitle>
<DialogContent> <DialogContent height="100%">
{content(closeModal)} {content(closeModal)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -164,4 +262,11 @@ ModalContainer.propTypes = {
id: PropTypes.string, id: PropTypes.string,
title: CustomPropTypes.children, title: CustomPropTypes.children,
content: PropTypes.func, 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 { 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 React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Theme from 'sources/Theme'; import Theme from 'sources/Theme';
@ -76,6 +83,41 @@ FinalNotifyContent.propTypes = {
children: CustomPropTypes.children, 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 = { var Notifier = {
success(msg, autoHideDuration = AUTO_HIDE_DURATION) { success(msg, autoHideDuration = AUTO_HIDE_DURATION) {
this._callNotify(msg, MESSAGE_TYPE.SUCCESS, autoHideDuration); this._callNotify(msg, MESSAGE_TYPE.SUCCESS, autoHideDuration);
@ -195,11 +237,11 @@ var Notifier = {
} }
modalRef.confirm(title, text, onOkClick, onCancelClick, okLabel, cancelLabel); modalRef.confirm(title, text, onOkClick, onCancelClick, okLabel, cancelLabel);
}, },
showModal(title, content) { showModal: (title, content, modalOptions) => {
if(!modalInitialized) { if(!modalInitialized) {
initializeModalProvider(); 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 { FileType } from 'react-aspen';
import { TreeNode } from './tree_nodes'; import { TreeNode } from './tree_nodes';
import {isValidData} from 'sources/utils'; import { isValidData } from 'sources/utils';
function manageTreeEvents(event, eventName, item) { 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 node;
let obj = pgAdmin.Browser; let obj = pgAdmin.Browser;
if (d && obj.Nodes[d._type]) { // Events for preferences tree.
node = obj.Nodes[d._type]; if (node_metadata.parent && node_metadata.parent.includes('/preferences') && obj.ptree.tree.type == 'preferences') {
// 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 { try {
node.trigger(
'browser-node.' + eventName, node, item, d
);
obj.Events.trigger( obj.Events.trigger(
'pgadmin-browser:tree:' + eventName, item, d, node 'preferences:tree:' + eventName, item, d
); );
} catch (e) { } catch (e) {
console.warn(e.stack || e); console.warn(e.stack || e);
return false; 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; return true;
} }
export class Tree { export class Tree {
constructor(tree, manageTree, pgBrowser) { constructor(tree, manageTree, pgBrowser, type) {
this.tree = tree; this.tree = tree;
this.tree.type = type ? type : 'browser';
this.tree.onTreeEvents(manageTreeEvents); this.tree.onTreeEvents(manageTreeEvents);
this.rootNode = manageTree.tempTree; this.rootNode = manageTree.tempTree;
@ -102,12 +118,12 @@ export class Tree {
} }
next(item) { next(item) {
if(item) { if (item) {
let parent = this.parent(item); let parent = this.parent(item);
if(parent && parent.children.length > 0) { if (parent && parent.children.length > 0) {
let idx = parent.children.indexOf(item); let idx = parent.children.indexOf(item);
if(idx !== -1 && parent.children.length !== idx+1) { if (idx !== -1 && parent.children.length !== idx + 1) {
return parent.children[idx+1]; return parent.children[idx + 1];
} }
} }
} }
@ -115,12 +131,12 @@ export class Tree {
} }
prev(item) { prev(item) {
if(item) { if (item) {
let parent = this.parent(item); let parent = this.parent(item);
if(parent && parent.children.length > 0) { if (parent && parent.children.length > 0) {
let idx = parent.children.indexOf(item); let idx = parent.children.indexOf(item);
if(idx !== -1 && idx !== 0) { if (idx !== -1 && idx !== 0) {
return parent.children[idx-1]; return parent.children[idx - 1];
} }
} }
} }
@ -136,7 +152,7 @@ export class Tree {
await item.ensureLoaded(); await item.ensureLoaded();
} }
async ensureVisible(item){ async ensureVisible(item) {
await this.tree.ensureVisible(item); await this.tree.ensureVisible(item);
} }
@ -153,11 +169,11 @@ export class Tree {
await this.tree.toggleDirectory(item); 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); 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); this.tree.setActiveFile(item, ensureVisible, align);
} }
@ -180,18 +196,18 @@ export class Tree {
// TBD // TBD
} }
async setLabel(item, label) { async setLabel(item, label) {
if(item) { if (item) {
await this.tree.setLabel(item, label); await this.tree.setLabel(item, label);
} }
} }
async setInode(item) { async setInode(item) {
if(item._children) item._children = null; if (item._children) item._children = null;
await this.tree.closeDirectory(item); await this.tree.closeDirectory(item);
} }
async setId(item, data) { async setId(item, data) {
if(item) { if (item) {
item.getMetadata('data').id = data.id; item.getMetadata('data').id = data.id;
} }
} }
@ -269,7 +285,7 @@ export class Tree {
siblings(item) { siblings(item) {
if (this.hasParent(item)) { if (this.hasParent(item)) {
let _siblings = this.parent(item).children.filter((_item) => _item.path !== item.path); 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; else return _siblings;
} }
return []; return [];
@ -294,7 +310,7 @@ export class Tree {
} }
itemData(item) { 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) { getData(item) {
@ -323,18 +339,18 @@ export class Tree {
findNodeWithToggle(path) { findNodeWithToggle(path) {
let tree = this; let tree = this;
if(path == null || !Array.isArray(path)) { if (path == null || !Array.isArray(path)) {
return Promise.reject(); return Promise.reject();
} }
path = '/browser/' + path.join('/'); path = '/browser/' + path.join('/');
let onCorrectPath = function(matchPath) { let onCorrectPath = function (matchPath) {
return (matchPath !== undefined && path !== undefined return (matchPath !== undefined && path !== undefined
&& (path.startsWith(matchPath) || path === matchPath)); && (path.startsWith(matchPath) || path === matchPath));
}; };
return (function findInNode(currentNode) { return (function findInNode(currentNode) {
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject) => {
if (path === null || path === undefined || path.length === 0) { if (path === null || path === undefined || path.length === 0) {
resolve(null); resolve(null);
} }
@ -347,18 +363,18 @@ export class Tree {
resolve(currentNode); resolve(currentNode);
} else { } else {
tree.open(currentNode) tree.open(currentNode)
.then(()=>{ .then(() => {
let children = currentNode.children; let children = currentNode.children;
for (let i = 0, length = children.length; i < length; i++) { for (let i = 0, length = children.length; i < length; i++) {
let childNode = children[i]; let childNode = children[i];
if(onCorrectPath(childNode.path)) { if (onCorrectPath(childNode.path)) {
resolve(findInNode(childNode)); resolve(findInNode(childNode));
return; return;
} }
} }
reject(null); reject(null);
}) })
.catch(()=>{ .catch(() => {
reject(null); reject(null);
}); });
} }
@ -368,7 +384,7 @@ export class Tree {
findNodeByDomElement(domElement) { findNodeByDomElement(domElement) {
const path = domElement.path; const path = domElement.path;
if(!path || !path[0]) { if (!path || !path[0]) {
return undefined; return undefined;
} }
@ -390,7 +406,7 @@ export class Tree {
createOrUpdateNode(id, data, parent, domNode) { createOrUpdateNode(id, data, parent, domNode) {
let oldNodePath = id; 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; oldNodePath = parent.path + '/' + id;
} }
const oldNode = this.findNode(oldNodePath); const oldNode = this.findNode(oldNodePath);
@ -456,14 +472,14 @@ export class Tree {
* cur is selection range of text after dropping. If returned as * cur is selection range of text after dropping. If returned as
* string, by default cursor will be set to the end of text * string, by default cursor will be set to the end of text
*/ */
registerDraggableType(typeOrTypeDict, dropDetailsFunc=null) { registerDraggableType(typeOrTypeDict, dropDetailsFunc = null) {
if(typeof typeOrTypeDict == 'object') { if (typeof typeOrTypeDict == 'object') {
Object.keys(typeOrTypeDict).forEach((type)=>{ Object.keys(typeOrTypeDict).forEach((type) => {
this.registerDraggableType(type, typeOrTypeDict[type]); this.registerDraggableType(type, typeOrTypeDict[type]);
}); });
} else { } else {
if(dropDetailsFunc != null) { if (dropDetailsFunc != null) {
typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type)=>{ typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type) => {
this.draggableTypes[type] = dropDetailsFunc; this.draggableTypes[type] = dropDetailsFunc;
}); });
} }
@ -471,7 +487,7 @@ export class Tree {
} }
getDraggable(type) { getDraggable(type) {
if(this.draggableTypes[type]) { if (this.draggableTypes[type]) {
return this.draggableTypes[type]; return this.draggableTypes[type];
} else { } else {
return null; return null;
@ -482,7 +498,7 @@ export class Tree {
let data = item.getMetadata('data'); let data = item.getMetadata('data');
let dropDetailsFunc = this.getDraggable(data._type); let dropDetailsFunc = this.getDraggable(data._type);
if(dropDetailsFunc != null) { if (dropDetailsFunc != null) {
/* addEventListener is used here because import jquery.drag.event /* addEventListener is used here because import jquery.drag.event
* overrides the dragstart event set using element.on('dragstart') * overrides the dragstart event set using element.on('dragstart')
@ -490,20 +506,20 @@ export class Tree {
*/ */
let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item)); let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item));
if(typeof dropDetails == 'string') { if (typeof dropDetails == 'string') {
dropDetails = { dropDetails = {
text:dropDetails, text: dropDetails,
cur:{ cur: {
from:dropDetails.length, from: dropDetails.length,
to: dropDetails.length, to: dropDetails.length,
}, },
}; };
} else { } else {
if(!dropDetails.cur) { if (!dropDetails.cur) {
dropDetails = { dropDetails = {
...dropDetails, ...dropDetails,
cur:{ cur: {
from:dropDetails.text.length, from: dropDetails.text.length,
to: dropDetails.text.length, to: dropDetails.text.length,
}, },
}; };
@ -512,14 +528,14 @@ export class Tree {
e.dataTransfer.setData('text', JSON.stringify(dropDetails)); e.dataTransfer.setData('text', JSON.stringify(dropDetails));
/* Required by Firefox */ /* Required by Firefox */
if(e.dataTransfer.dropEffect) { if (e.dataTransfer.dropEffect) {
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move';
} }
/* setDragImage is not supported in IE. We leave it to /* setDragImage is not supported in IE. We leave it to
* its default look and feel * its default look and feel
*/ */
if(e.dataTransfer.setDragImage) { if (e.dataTransfer.setDragImage) {
let dragItem = $(` let dragItem = $(`
<div class="drag-tree-node"> <div class="drag-tree-node">
<span>${_.escape(dropDetails.text)}</span> <span>${_.escape(dropDetails.text)}</span>
@ -576,4 +592,4 @@ export function findInTree(rootNode, path) {
let isValidTreeNodeData = isValidData; let isValidTreeNodeData = isValidData;
export {isValidTreeNodeData}; export { isValidTreeNodeData };

View File

@ -1,5 +1,8 @@
/* Overrides alertify js headers */ /* Overrides alertify js headers */
.alertify { .alertify {
z-index: 3000;
position: fixed;
.ajs-header { .ajs-header {
padding: 6px 10px !important; padding: 6px 10px !important;
//margin is calculated with -$alertify-borderremove-margin, adjust the header //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'}, options=[{'label': gettext('None'), 'value': 'none'},
{'label': gettext('All'), 'value': 'all'}, {'label': gettext('All'), 'value': 'all'},
{'label': gettext('Strings'), 'value': 'strings'}], {'label': gettext('Strings'), 'value': 'strings'}],
select2={ control_props={
'allowClear': False, 'allowClear': False,
'tags': False 'tags': False
} }
@ -209,9 +209,9 @@ def register_query_tool_preferences(self):
category_label=PREF_LABEL_CSV_TXT, category_label=PREF_LABEL_CSV_TXT,
options=[{'label': '"', 'value': '"'}, options=[{'label': '"', 'value': '"'},
{'label': '\'', 'value': '\''}], {'label': '\'', 'value': '\''}],
select2={ control_props={
'allowClear': False, 'allowClear': False,
'tags': True 'tags': False
} }
) )
@ -223,9 +223,9 @@ def register_query_tool_preferences(self):
{'label': ',', 'value': ','}, {'label': ',', 'value': ','},
{'label': '|', 'value': '|'}, {'label': '|', 'value': '|'},
{'label': gettext('Tab'), 'value': '\t'}], {'label': gettext('Tab'), 'value': '\t'}],
select2={ control_props={
'allowClear': False, 'allowClear': False,
'tags': True 'tags': False
} }
) )
@ -247,7 +247,7 @@ def register_query_tool_preferences(self):
options=[{'label': gettext('None'), 'value': 'none'}, options=[{'label': gettext('None'), 'value': 'none'},
{'label': gettext('All'), 'value': 'all'}, {'label': gettext('All'), 'value': 'all'},
{'label': gettext('Strings'), 'value': 'strings'}], {'label': gettext('Strings'), 'value': 'strings'}],
select2={ control_props={
'allowClear': False, 'allowClear': False,
'tags': False 'tags': False
} }
@ -259,9 +259,9 @@ def register_query_tool_preferences(self):
category_label=PREF_LABEL_RESULTS_GRID, category_label=PREF_LABEL_RESULTS_GRID,
options=[{'label': '"', 'value': '"'}, options=[{'label': '"', 'value': '"'},
{'label': '\'', 'value': '\''}], {'label': '\'', 'value': '\''}],
select2={ control_props={
'allowClear': False, 'allowClear': False,
'tags': True 'tags': False
} }
) )
@ -273,9 +273,9 @@ def register_query_tool_preferences(self):
{'label': ',', 'value': ','}, {'label': ',', 'value': ','},
{'label': '|', 'value': '|'}, {'label': '|', 'value': '|'},
{'label': gettext('Tab'), 'value': '\t'}], {'label': gettext('Tab'), 'value': '\t'}],
select2={ control_props={
'allowClear': False, 'allowClear': False,
'tags': True 'tags': False
} }
) )

View File

@ -66,10 +66,11 @@ class _Preference(object):
self.label = label self.label = label
self._type = _type self._type = _type
self.help_str = kwargs.get('help_str', None) 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.min_val = kwargs.get('min_val', None)
self.max_val = kwargs.get('max_val', None) self.max_val = kwargs.get('max_val', None)
self.options = kwargs.get('options', 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.fields = kwargs.get('fields', None)
self.allow_blanks = kwargs.get('allow_blanks', None) self.allow_blanks = kwargs.get('allow_blanks', None)
self.disabled = kwargs.get('disabled', False) self.disabled = kwargs.get('disabled', False)
@ -146,10 +147,10 @@ class _Preference(object):
for opt in self.options: for opt in self.options:
if 'value' in opt and opt['value'] == res.value: if 'value' in opt and opt['value'] == res.value:
return True, 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, res.value
return True, self.default return True, self.default
if self._type == 'select2': if self._type == 'select':
if res.value: if res.value:
res.value = res.value.replace('[', '') res.value = res.value.replace('[', '')
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 has_value = next((True for opt in self.options
if 'value' in opt and opt['value'] == value), if 'value' in opt and opt['value'] == value),
False) 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': elif self._type == 'date':
value = parser_map[self._type](value).date() value = parser_map[self._type](value).date()
else: else:
@ -248,10 +249,11 @@ class _Preference(object):
'label': self.label or self.name, 'label': self.label or self.name,
'type': self._type, 'type': self._type,
'help_str': self.help_str, 'help_str': self.help_str,
'control_props': self.control_props,
'min_val': self.min_val, 'min_val': self.min_val,
'max_val': self.max_val, 'max_val': self.max_val,
'options': self.options, 'options': self.options,
'select2': self.select2, 'select': self.select,
'value': self.get(), 'value': self.get(),
'fields': self.fields, 'fields': self.fields,
'disabled': self.disabled, 'disabled': self.disabled,
@ -393,7 +395,7 @@ class Preferences(object):
return res return res
def register( def register(
self, category, name, label, _type, default, **kwargs self, category, name, label, _type, default, **kwargs
): ):
""" """
register register
@ -414,7 +416,7 @@ class Preferences(object):
:param options: :param options:
:param help_str: :param help_str:
:param category_label: :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 :param fields: field schema (if preference has more than one field to
take input from user e.g. keyboardshortcut preference) take input from user e.g. keyboardshortcut preference)
:param allow_blanks: Flag specify whether to allow blank value. :param allow_blanks: Flag specify whether to allow blank value.
@ -424,8 +426,9 @@ class Preferences(object):
max_val = kwargs.get('max_val', None) max_val = kwargs.get('max_val', None)
options = kwargs.get('options', None) options = kwargs.get('options', None)
help_str = kwargs.get('help_str', None) help_str = kwargs.get('help_str', None)
control_props = kwargs.get('control_props', {})
category_label = kwargs.get('category_label', None) category_label = kwargs.get('category_label', None)
select2 = kwargs.get('select2', None) select = kwargs.get('select', None)
fields = kwargs.get('fields', None) fields = kwargs.get('fields', None)
allow_blanks = kwargs.get('allow_blanks', None) allow_blanks = kwargs.get('allow_blanks', None)
disabled = kwargs.get('disabled', False) disabled = kwargs.get('disabled', False)
@ -440,14 +443,15 @@ class Preferences(object):
assert _type in ( assert _type in (
'boolean', 'integer', 'numeric', 'date', 'datetime', 'boolean', 'integer', 'numeric', 'date', 'datetime',
'options', 'multiline', 'switch', 'node', 'text', 'radioModern', 'options', 'multiline', 'switch', 'node', 'text', 'radioModern',
'keyboardshortcut', 'select2', 'selectFile', 'threshold' 'keyboardshortcut', 'select', 'selectFile', 'threshold'
), "Type cannot be found in the defined list!" ), "Type cannot be found in the defined list!"
(cat['preferences'])[name] = res = _Preference( (cat['preferences'])[name] = res = _Preference(
cat['id'], name, label, _type, default, help_str=help_str, cat['id'], name, label, _type, default, help_str=help_str,
min_val=min_val, max_val=max_val, options=options, min_val=min_val, max_val=max_val, options=options,
select2=select2, fields=fields, allow_blanks=allow_blanks, select=select, fields=fields, allow_blanks=allow_blanks,
disabled=disabled, dependents=dependents disabled=disabled, dependents=dependents,
control_props=control_props
) )
return res return res
@ -483,7 +487,7 @@ class Preferences(object):
@classmethod @classmethod
def register_preference( def register_preference(
cls, module, category, name, label, _type, **kwargs cls, module, category, name, label, _type, **kwargs
): ):
""" """
register register
@ -503,6 +507,7 @@ class Preferences(object):
max_val = kwargs.get('max_val', None) max_val = kwargs.get('max_val', None)
options = kwargs.get('options', None) options = kwargs.get('options', None)
help_str = kwargs.get('help_str', None) help_str = kwargs.get('help_str', None)
control_props = kwargs.get('control_props', None)
module_label = kwargs.get('module_label', None) module_label = kwargs.get('module_label', None)
category_label = kwargs.get('category_label', None) category_label = kwargs.get('category_label', None)
@ -516,6 +521,7 @@ class Preferences(object):
return m.register( return m.register(
category, name, label, _type, default, min_val=min_val, category, name, label, _type, default, min_val=min_val,
max_val=max_val, options=options, help_str=help_str, max_val=max_val, options=options, help_str=help_str,
control_props=control_props,
category_label=category_label 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.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.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.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.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'),
'pgadmin.server.supported_servers': '/browser/server/supported_servers', 'pgadmin.server.supported_servers': '/browser/server/supported_servers',
'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'), 'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'),

View File

@ -91,19 +91,19 @@
json5 "^2.1.2" json5 "^2.1.2"
semver "^6.3.0" semver "^6.3.0"
"@babel/eslint-parser@^7.12.13": "@babel/eslint-parser@^7.17.0":
version "7.13.8" version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.13.8.tgz#6f2bde6b0690fcc0598b4869fc7c8e8b55b17687" resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz#eabb24ad9f0afa80e5849f8240d0e5facc2d90d6"
integrity sha512-XewKkiyukrGzMeqToXJQk6hjg2veI9SNQElGzAoAjKxYCLbgcVX4KA2WhoyqMon9N4RMdCZhNTJNOBcp9spsiw== integrity sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==
dependencies: dependencies:
eslint-scope "5.1.0" eslint-scope "^5.1.1"
eslint-visitor-keys "^1.3.0" eslint-visitor-keys "^2.1.0"
semver "^6.3.0" semver "^6.3.0"
"@babel/eslint-plugin@^7.12.13": "@babel/eslint-plugin@^7.17.7":
version "7.13.0" version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.13.0.tgz#e6d99efcd6b8551adf479e382a47218726179b1b" resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.17.7.tgz#4ee1d5b29b79130f3bb5a933358376bcbee172b8"
integrity sha512-YGwCLc/u/uc3bU+q/fvgRQ62+TkxuyVvdmybK6ElzE49vODp+RnRe16eJzMM7EwvcRPQfQvcOSuGmzfcbZE2+w== integrity sha512-JATUoJJXSgwI0T8juxWYtK1JSgoLpIGUsCHIv+NMXcUDA2vIe6nvAHR9vnuJgs/P1hOFw7vPwibixzfqBBLIVw==
dependencies: dependencies:
eslint-rule-composer "^0.3.0" 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" resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"
integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== 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: eslint-scope@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" 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" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== 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" version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
@ -5320,7 +5312,7 @@ esquery@^1.4.0:
dependencies: dependencies:
estraverse "^5.1.0" estraverse "^5.1.0"
esrecurse@^4.1.0, esrecurse@^4.3.0: esrecurse@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 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" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= 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: fast-safe-stringify@^2.0.7:
version "2.0.7" version "2.0.7"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" 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" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 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" version "1.0.0"
resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422" resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613"
dependencies: dependencies:
"@types/classnames" "^2.2.6" "@types/classnames" "^2.2.6"
"@types/react" "^16.7.18" "@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" react-is "^16.12.0"
shallowequal "^1.1.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: react-aspen@^1.1.0, react-aspen@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/react-aspen/-/react-aspen-1.1.1.tgz#61a85ef43748158322c4a3b73faaa5e563edd038" 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" object-assign "^4.1.1"
scheduler "^0.20.2" 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: react-draggable@^4.4.4:
version "4.4.4" version "4.4.4"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f" 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" resolved "https://registry.yarnpkg.com/react-property/-/react-property-1.0.1.tgz#4ae4211557d0a0ae050a71aa8ad288c074bea4e6"
integrity sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ== 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: react-select@^4.2.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" 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" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= 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: tslib@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"