Port preferences dialog to React. Fixes #7149
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 179 KiB |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 157 KiB |
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 138 KiB |
Before Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 142 KiB |
BIN
docs/en_US/images/preferences_erd_keyboard_shortcuts.png
Normal file
After Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 246 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 183 KiB |
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 92 KiB |
@ -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
|
||||||
**********************
|
**********************
|
||||||
|
|
||||||
|
@ -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
|
||||||
*********
|
*********
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
};
|
@ -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,
|
||||||
|
};
|
21
web/pgadmin/preferences/static/js/index.js
Normal 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,
|
||||||
|
};
|
@ -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;
|
|
||||||
});
|
|
||||||
|
8
web/pgadmin/preferences/tests/__init__.py
Normal 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
|
||||||
|
#
|
||||||
|
##########################################################################
|
27
web/pgadmin/preferences/tests/preferences_test_data.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
39
web/pgadmin/preferences/tests/test_preferences_get.py
Normal 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)
|
60
web/pgadmin/preferences/tests/test_preferences_update.py
Normal 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')
|
1
web/pgadmin/static/img/fonticon/expand.svg
Normal 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 |
1
web/pgadmin/static/img/fonticon/minimize_collapse.svg
Normal 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 |
@ -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]),
|
||||||
|
@ -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))} />;
|
||||||
};
|
};
|
||||||
|
@ -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 */
|
||||||
|
@ -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'}} />;
|
||||||
|
|
||||||
|
@ -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'}}/> {option.label}
|
<CheckRoundedIcon style={{ visibility: isSelected ? 'visible' : 'hidden' }} /> {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>
|
||||||
);
|
);
|
||||||
|
130
web/pgadmin/static/js/components/KeyboardShortcuts.jsx
Normal 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
|
||||||
|
};
|
90
web/pgadmin/static/js/components/QueryThresholds.jsx
Normal 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,
|
||||||
|
};
|
58
web/pgadmin/static/js/components/Themes.jsx
Normal 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
|
||||||
|
};
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
247
web/pgadmin/static/js/tree/preference_nodes.ts
Normal 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;
|
||||||
|
}
|
53
web/pgadmin/static/js/tree/preferences_tree.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -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 };
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
100
web/regression/javascript/components/KeyboardShortcuts.spec.js
Normal 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
86
web/regression/javascript/components/QueryThreshold.spec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
96
web/regression/javascript/components/Themes.spec.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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'),
|
||||||
|
@ -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"
|
||||||
|