diff --git a/docs/en_US/images/preferences_browser_display.png b/docs/en_US/images/preferences_browser_display.png index 8fe9b7106..e2da64e91 100644 Binary files a/docs/en_US/images/preferences_browser_display.png and b/docs/en_US/images/preferences_browser_display.png differ diff --git a/docs/en_US/images/preferences_browser_keyboard_shortcuts.png b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png index 6b16a19e0..773526ea1 100644 Binary files a/docs/en_US/images/preferences_browser_keyboard_shortcuts.png and b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_browser_nodes.png b/docs/en_US/images/preferences_browser_nodes.png index bbe9ff2a3..e3a44d33b 100644 Binary files a/docs/en_US/images/preferences_browser_nodes.png and b/docs/en_US/images/preferences_browser_nodes.png differ diff --git a/docs/en_US/images/preferences_browser_properties.png b/docs/en_US/images/preferences_browser_properties.png index 7d1690bc7..a5edd85e4 100644 Binary files a/docs/en_US/images/preferences_browser_properties.png and b/docs/en_US/images/preferences_browser_properties.png differ diff --git a/docs/en_US/images/preferences_browser_tab_settings.png b/docs/en_US/images/preferences_browser_tab_settings.png index 316d089f9..35c26a52a 100644 Binary files a/docs/en_US/images/preferences_browser_tab_settings.png and b/docs/en_US/images/preferences_browser_tab_settings.png differ diff --git a/docs/en_US/images/preferences_dashboard_display.png b/docs/en_US/images/preferences_dashboard_display.png index b7a0f1a36..6d67e23de 100644 Binary files a/docs/en_US/images/preferences_dashboard_display.png and b/docs/en_US/images/preferences_dashboard_display.png differ diff --git a/docs/en_US/images/preferences_dashboard_graphs.png b/docs/en_US/images/preferences_dashboard_graphs.png index ca89a0333..df3ea7b2b 100644 Binary files a/docs/en_US/images/preferences_dashboard_graphs.png and b/docs/en_US/images/preferences_dashboard_graphs.png differ diff --git a/docs/en_US/images/preferences_debugger_display.png b/docs/en_US/images/preferences_debugger_display.png deleted file mode 100644 index 243f71f03..000000000 Binary files a/docs/en_US/images/preferences_debugger_display.png and /dev/null differ diff --git a/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png b/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png index 772223e38..defa03ae5 100644 Binary files a/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png and b/docs/en_US/images/preferences_debugger_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_erd_keyboard_shortcuts.png b/docs/en_US/images/preferences_erd_keyboard_shortcuts.png new file mode 100644 index 000000000..9ae1d0044 Binary files /dev/null and b/docs/en_US/images/preferences_erd_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_misc_themes.png b/docs/en_US/images/preferences_misc_themes.png index a606b6684..217ddc0c5 100644 Binary files a/docs/en_US/images/preferences_misc_themes.png and b/docs/en_US/images/preferences_misc_themes.png differ diff --git a/docs/en_US/images/preferences_misc_user_language.png b/docs/en_US/images/preferences_misc_user_language.png index 7529694b2..0d5a186e4 100644 Binary files a/docs/en_US/images/preferences_misc_user_language.png and b/docs/en_US/images/preferences_misc_user_language.png differ diff --git a/docs/en_US/images/preferences_paths_binary.png b/docs/en_US/images/preferences_paths_binary.png index 2bb37d117..defceed3d 100644 Binary files a/docs/en_US/images/preferences_paths_binary.png and b/docs/en_US/images/preferences_paths_binary.png differ diff --git a/docs/en_US/images/preferences_paths_help.png b/docs/en_US/images/preferences_paths_help.png index 6cffc0f38..a40cc2589 100644 Binary files a/docs/en_US/images/preferences_paths_help.png and b/docs/en_US/images/preferences_paths_help.png differ diff --git a/docs/en_US/images/preferences_schema_diff.png b/docs/en_US/images/preferences_schema_diff.png index d95730795..c5eea4d6b 100644 Binary files a/docs/en_US/images/preferences_schema_diff.png and b/docs/en_US/images/preferences_schema_diff.png differ diff --git a/docs/en_US/images/preferences_sql_auto_completion.png b/docs/en_US/images/preferences_sql_auto_completion.png index 16a82c1f3..d69cd16d9 100644 Binary files a/docs/en_US/images/preferences_sql_auto_completion.png and b/docs/en_US/images/preferences_sql_auto_completion.png differ diff --git a/docs/en_US/images/preferences_sql_csv_output.png b/docs/en_US/images/preferences_sql_csv_output.png index 9c7c37969..0bbd1b724 100644 Binary files a/docs/en_US/images/preferences_sql_csv_output.png and b/docs/en_US/images/preferences_sql_csv_output.png differ diff --git a/docs/en_US/images/preferences_sql_display.png b/docs/en_US/images/preferences_sql_display.png index 97e8ae4a2..7b1169088 100644 Binary files a/docs/en_US/images/preferences_sql_display.png and b/docs/en_US/images/preferences_sql_display.png differ diff --git a/docs/en_US/images/preferences_sql_editor.png b/docs/en_US/images/preferences_sql_editor.png index 52194a66c..57cfa31f4 100644 Binary files a/docs/en_US/images/preferences_sql_editor.png and b/docs/en_US/images/preferences_sql_editor.png differ diff --git a/docs/en_US/images/preferences_sql_explain.png b/docs/en_US/images/preferences_sql_explain.png index f38b1ed61..1fab85096 100644 Binary files a/docs/en_US/images/preferences_sql_explain.png and b/docs/en_US/images/preferences_sql_explain.png differ diff --git a/docs/en_US/images/preferences_sql_formatting.png b/docs/en_US/images/preferences_sql_formatting.png index 7d85cfcf8..75a844887 100644 Binary files a/docs/en_US/images/preferences_sql_formatting.png and b/docs/en_US/images/preferences_sql_formatting.png differ diff --git a/docs/en_US/images/preferences_sql_keyboard_shortcuts.png b/docs/en_US/images/preferences_sql_keyboard_shortcuts.png index 25e1270d4..84e225b7f 100644 Binary files a/docs/en_US/images/preferences_sql_keyboard_shortcuts.png and b/docs/en_US/images/preferences_sql_keyboard_shortcuts.png differ diff --git a/docs/en_US/images/preferences_sql_options.png b/docs/en_US/images/preferences_sql_options.png index 7f552d2ba..48a6d2477 100644 Binary files a/docs/en_US/images/preferences_sql_options.png and b/docs/en_US/images/preferences_sql_options.png differ diff --git a/docs/en_US/images/preferences_sql_results_grid.png b/docs/en_US/images/preferences_sql_results_grid.png index 15eaf9cdc..da0b50a98 100644 Binary files a/docs/en_US/images/preferences_sql_results_grid.png and b/docs/en_US/images/preferences_sql_results_grid.png differ diff --git a/docs/en_US/images/preferences_storage_options.png b/docs/en_US/images/preferences_storage_options.png index f3c44631e..4486b2901 100644 Binary files a/docs/en_US/images/preferences_storage_options.png and b/docs/en_US/images/preferences_storage_options.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 925e4a214..66e0b9600 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -182,6 +182,18 @@ debugger window navigation: :alt: Preferences dialog debugger keyboard shortcuts section :align: center +The ERD Tool Node +***************** + +Expand the *ERD Tool* node to specify your ERD Tool display preferences. + +Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the +ERD Tool window navigation: + +.. image:: images/preferences_erd_keyboard_shortcuts.png + :alt: Preferences dialog erd keyboard shortcuts section + :align: center + The Miscellaneous Node ********************** diff --git a/docs/en_US/release_notes_6_8.rst b/docs/en_US/release_notes_6_8.rst index 6f04d72ef..4e878fbb7 100644 --- a/docs/en_US/release_notes_6_8.rst +++ b/docs/en_US/release_notes_6_8.rst @@ -14,7 +14,7 @@ New features Housekeeping ************ - + | `Issue #7149 `_ - Port preferences dialog to React. Bug fixes ********* diff --git a/web/package.json b/web/package.json index e4a27bd10..4200b1f82 100644 --- a/web/package.json +++ b/web/package.json @@ -8,8 +8,8 @@ "license": "PostgreSQL", "devDependencies": { "@babel/core": "^7.10.2", - "@babel/eslint-parser": "^7.12.13", - "@babel/eslint-plugin": "^7.12.13", + "@babel/eslint-parser": "^7.17.0", + "@babel/eslint-plugin": "^7.17.7", "@babel/plugin-proposal-object-rest-spread": "^7.10.1", "@babel/plugin-syntax-jsx": "^7.16.0", "@babel/preset-env": "^7.10.2", @@ -145,7 +145,7 @@ "path-fx": "^2.0.0", "pathfinding": "^0.4.18", "paths-js": "^0.4.9", - "pgadmin4-tree": "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422", + "pgadmin4-tree": "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613", "postcss": "^8.2.15", "raf": "^3.4.1", "rc-dock": "^3.2.9", @@ -154,6 +154,7 @@ "react-checkbox-tree": "^1.7.2", "react-dom": "^17.0.1", "react-draggable": "^4.4.4", + "react-rnd": "^10.3.5", "react-select": "^4.2.1", "react-table": "^7.6.3", "react-timer-hook": "^3.0.5", diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py index af7dc4f2c..0b0224b3d 100644 --- a/web/pgadmin/browser/register_browser_preferences.py +++ b/web/pgadmin/browser/register_browser_preferences.py @@ -519,7 +519,7 @@ def register_browser_preferences(self): self.open_in_new_tab = self.preference.register( 'tab_settings', 'new_browser_tab_open', - gettext("Open in new browser tab"), 'select2', None, + gettext("Open in new browser tab"), 'select', None, category_label=PREF_LABEL_OPTIONS, options=ope_new_tab_options, help_str=gettext( @@ -527,7 +527,7 @@ def register_browser_preferences(self): 'or PSQL Tool from the drop-down to set ' 'open in new browser tab for that particular module.' ), - select2={ + control_props={ 'multiple': True, 'allowClear': False, 'tags': True, 'first_empty': False, 'selectOnClose': False, 'emptyOptions': True, diff --git a/web/pgadmin/browser/server_groups/servers/static/js/binary_path.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/binary_path.ui.js new file mode 100644 index 000000000..058c3b79c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/static/js/binary_path.ui.js @@ -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; + } + }, + ]; + } +} diff --git a/web/pgadmin/misc/__init__.py b/web/pgadmin/misc/__init__.py index fb436d708..1acb64dd2 100644 --- a/web/pgadmin/misc/__init__.py +++ b/web/pgadmin/misc/__init__.py @@ -67,7 +67,10 @@ class MiscModule(PgAdminModule): 'user_language', 'user_language', gettext("User language"), 'options', 'en', category_label=gettext('User language'), - options=lang_options + options=lang_options, + control_props={ + 'allowClear': False, + } ) theme_options = [] @@ -90,8 +93,11 @@ class MiscModule(PgAdminModule): gettext("Theme"), 'options', 'standard', category_label=gettext('Themes'), options=theme_options, + control_props={ + 'allowClear': False, + }, help_str=gettext( - 'A refresh is required to apply the theme. Below is the ' + 'A refresh is required to apply the theme. Above is the ' 'preview of the theme' ) ) diff --git a/web/pgadmin/misc/file_manager/__init__.py b/web/pgadmin/misc/file_manager/__init__.py index 9df8ccc41..77754cf44 100644 --- a/web/pgadmin/misc/file_manager/__init__.py +++ b/web/pgadmin/misc/file_manager/__init__.py @@ -167,10 +167,14 @@ class FileManagerModule(PgAdminModule): ) self.file_dialog_view = self.preference.register( 'options', 'file_dialog_view', - gettext("File dialog view"), 'options', 'list', + gettext("File dialog view"), 'select', 'list', category_label=PREF_LABEL_OPTIONS, options=[{'label': gettext('List'), 'value': 'list'}, - {'label': gettext('Grid'), 'value': 'grid'}] + {'label': gettext('Grid'), 'value': 'grid'}], + control_props={ + 'allowClear': False, + 'tags': False + }, ) self.show_hidden_files = self.preference.register( 'options', 'show_hidden_files', @@ -236,7 +240,7 @@ def file_manager_config(trans_id): """render the required json""" data = Filemanager.get_trasaction_selection(trans_id) pref = Preferences.module('file_manager') - file_dialog_view = pref.preference('file_dialog_view').get() + file_dialog_view = pref.preference('file_dialog_view').get()[0] show_hidden_files = pref.preference('show_hidden_files').get() return Response(response=render_template( diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py index 9872a6724..a84731336 100644 --- a/web/pgadmin/preferences/__init__.py +++ b/web/pgadmin/preferences/__init__.py @@ -37,26 +37,23 @@ class PreferencesModule(PgAdminModule): """ def get_own_javascripts(self): - return [{ - 'name': 'pgadmin.preferences', - 'path': url_for('preferences.index') + 'preferences', - 'when': None - }] + scripts = list() + for name, script in [ + ['pgadmin.preferences', 'js/preferences'] + ]: + scripts.append({ + 'name': name, + 'path': url_for('preferences.index') + script, + 'when': None + }) + + return scripts def get_own_stylesheets(self): return [] def get_own_menuitems(self): - return { - 'file_items': [ - MenuItem(name='mnu_preferences', - priority=997, - module="pgAdmin.Preferences", - callback='show', - icon='fa fa-cog', - label=gettext('Preferences')) - ] - } + return {} def get_exposed_url_endpoints(self): """ @@ -149,7 +146,8 @@ def _iterate_categories(pref_d, label, res): "label": gettext(pref_d['label']), "inode": True, "open": True, - "branch": [] + "children": [], + "value": gettext(pref_d['label']), } for c in pref_d['categories']: @@ -162,13 +160,15 @@ def _iterate_categories(pref_d, label, res): "id": c['id'], "mid": pref_d['id'], "label": gettext(c['label']), + "value": '{0}{1}'.format(c['id'], gettext(c['label'])), "inode": False, "open": False, - "preferences": sorted(c['preferences'], key=label) + "preferences": sorted(c['preferences'], key=label), + "showCheckbox": False } - (om['branch']).append(oc) - om['branch'] = sorted(om['branch'], key=label) + (om['children']).append(oc) + om['children'] = sorted(om['children'], key=label) res.append(om) @@ -194,53 +194,69 @@ def preferences_s(): ) -@blueprint.route("/", methods=["PUT"], endpoint="update") +def get_data(): + """ + Get preferences data. + :return: Preferences list + :rtype: list + """ + pref_data = request.form if request.form else json.loads( + request.data.decode()) + + if not pref_data: + raise ValueError("Please provide the valid preferences data to save.") + + return pref_data + + +@blueprint.route("/", methods=["PUT"], endpoint="update") @login_required -def save(pid): +def save(): """ Save a specific preference. """ - data = request.form if request.form else json.loads(request.data.decode()) + pref_data = get_data() - if data['name'] in ['vw_edt_tab_title_placeholder', - 'qt_tab_title_placeholder', - 'debugger_tab_title_placeholder'] \ - and data['value'].isspace(): - data['value'] = '' + for data in pref_data: + if data['name'] in ['vw_edt_tab_title_placeholder', + 'qt_tab_title_placeholder', + 'debugger_tab_title_placeholder'] \ + and data['value'].isspace(): + data['value'] = '' - res, msg = Preferences.save( - data['mid'], data['category_id'], data['id'], data['value']) - sgm.get_nodes(sgm) + res, msg = Preferences.save( + data['mid'], data['category_id'], data['id'], data['value']) + sgm.get_nodes(sgm) - if not res: - return internal_server_error(errormsg=msg) + if not res: + return internal_server_error(errormsg=msg) - response = success_return() + response = success_return() - # Set cookie & session for language settings. - # This will execute every time as could not find the better way to know - # that which preference is getting updated. + # Set cookie & session for language settings. + # This will execute every time as could not find the better way to know + # that which preference is getting updated. - misc_preference = Preferences.module('misc') - user_languages = misc_preference.preference( - 'user_language' - ) + misc_preference = Preferences.module('misc') + user_languages = misc_preference.preference( + 'user_language' + ) - language = 'en' - if user_languages: - language = user_languages.get() or language + language = 'en' + if user_languages: + language = user_languages.get() or language - domain = dict() - if config.COOKIE_DEFAULT_DOMAIN and\ - config.COOKIE_DEFAULT_DOMAIN != 'localhost': - domain['domain'] = config.COOKIE_DEFAULT_DOMAIN + domain = dict() + if config.COOKIE_DEFAULT_DOMAIN and \ + config.COOKIE_DEFAULT_DOMAIN != 'localhost': + domain['domain'] = config.COOKIE_DEFAULT_DOMAIN - setattr(session, 'PGADMIN_LANGUAGE', language) - response.set_cookie("PGADMIN_LANGUAGE", value=language, - path=config.COOKIE_DEFAULT_PATH, - secure=config.SESSION_COOKIE_SECURE, - httponly=config.SESSION_COOKIE_HTTPONLY, - samesite=config.SESSION_COOKIE_SAMESITE, - **domain) + setattr(session, 'PGADMIN_LANGUAGE', language) + response.set_cookie("PGADMIN_LANGUAGE", value=language, + path=config.COOKIE_DEFAULT_PATH, + secure=config.SESSION_COOKIE_SECURE, + httponly=config.SESSION_COOKIE_HTTPONLY, + samesite=config.SESSION_COOKIE_SAMESITE, + **domain) return response diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx new file mode 100644 index 000000000..7f4662771 --- /dev/null +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -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 ( + { + 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: [ + '
  • ', + gettext(note), + '
', + ].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 ( + + + + + + + + { + prefSchema.current && loadTree > 0 ? + { + Object.keys(changedData).length > 0 ? setDisableSave(false) : setDisableSave(true); + prefChangedData.current = changedData; + }}> + : <> + } + + + + + } title={gettext('Help for this dialog.')} /> + + + { props.closeModal(); /*props.panel.close()*/ }} startIcon={ { props.closeModal(); /*props.panel.close()*/ }} />}> + {gettext('Cancel')} + + } disabled={disableSave} onClick={() => { savePreferences(prefChangedData, initValues); }}> + {gettext('Save')} + + + + {/* */} + + + + ); +} + +PreferencesComponent.propTypes = { + schema: PropTypes.array, + initValues: PropTypes.object, + closeModal: PropTypes.func, + renderTree: PropTypes.func +}; diff --git a/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx b/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx new file mode 100644 index 000000000..de97b7ac7 --- /dev/null +++ b/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx @@ -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( + + , containerElement); +}; + +module.exports = { + initPreferencesTree: initPreferencesTree, +}; \ No newline at end of file diff --git a/web/pgadmin/preferences/static/js/index.js b/web/pgadmin/preferences/static/js/index.js new file mode 100644 index 000000000..2764633f7 --- /dev/null +++ b/web/pgadmin/preferences/static/js/index.js @@ -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, +}; diff --git a/web/pgadmin/preferences/static/js/preferences.js b/web/pgadmin/preferences/static/js/preferences.js index ebef31cad..2d4fa6c57 100644 --- a/web/pgadmin/preferences/static/js/preferences.js +++ b/web/pgadmin/preferences/static/js/preferences.js @@ -7,624 +7,55 @@ // ////////////////////////////////////////////////////////////// +import React from 'react'; +import gettext from 'sources/gettext'; +import PreferencesComponent from './components/PreferencesComponent'; import Notify from '../../../static/js/helpers/Notifier'; +// import PreferencesTree from './components/PreferencesTree'; +import { initPreferencesTree } from './components/PreferencesTree'; -define('pgadmin.preferences', [ - 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone', - 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.backform', - 'pgadmin.browser', 'sources/modify_animation', - 'tools/datagrid/static/js/show_query_tool', - 'sources/tree/pgadmin_tree_save_state', -], function( - gettext, url_for, $, _, Backbone, Alertify, pgAdmin, Backform, pgBrowser, - modifyAnimation, showQueryTool -) { - // This defines the Preference/Options Dialog for pgAdmin IV. +export default class Preferences { + static instance; - /* - * Hmm... this module is already been initialized, we can refer to the old - * object from here. - */ - if (pgAdmin.Preferences) - return pgAdmin.Preferences; + static getInstance(...args) { + if (!Preferences.instance) { + Preferences.instance = new Preferences(...args); + } + return Preferences.instance; + } - pgAdmin.Preferences = { - init: function() { - if (this.initialized) - return; + constructor(pgAdmin, pgBrowser) { + this.pgAdmin = pgAdmin; + this.pgBrowser = pgBrowser; + } - this.initialized = true; + init() { + if (this.initialized) + return; + this.initialized = true; + // Add Preferences in to file menu + var menus = [{ + name: 'mnu_preferences', + module: this, + applies: ['file'], + callback: 'show', + enable: true, + priority: 3, + label: gettext('Preferences'), + icon: 'fa fa-cog', + }]; - // Declare the Preferences dialog - Alertify.dialog('preferencesDlg', function() { + this.pgBrowser.add_menus(menus); + } - var jTree, // Variable to create the aci-tree - controls = [], // Keep tracking of all the backform controls - // created by the dialog. - // Dialog containter - $container = $('
'); - - - /* - * Preference Model - * - * This model will be used to keep tracking of the changes done for - * an individual option. - */ - var PreferenceModel = Backbone.Model.extend({ - idAttribute: 'id', - defaults: { - id: undefined, - value: undefined, - }, - }); - - /* - * Preferences Collection object. - * - * We will use only one collection object to keep track of all the - * preferences. - */ - var changed = {}, - preferences = this.preferences = new(Backbone.Collection.extend({ - model: PreferenceModel, - url: url_for('preferences.index'), - updateAll: function() { - // We will send only the modified data to the server. - for (var key in changed) { - this.get(key).save(); - } - - return true; - }, - }))(null); - - preferences.on('reset', function() { - // Reset the changed variables - changed = {}; - }); - - preferences.on('change', function(m) { - var id = m.get('id'), - dependents = m.get('dependents'); - if (!(id in changed)) { - // Keep track of the original value - changed[id] = m._previousAttributes.value; - } else if (_.isEqual(m.get('value'), changed[id])) { - // Remove unchanged models. - delete changed[id]; - } - - // Check dependents exist or not. If exists then call dependentsFound function. - if (!_.isNull(dependents) && Array.isArray(dependents) && dependents.length > 0) { - dependentsFound(m.get('name'), m.get('value'), dependents); - } - }); - - /* - * Function: dependentsFound - * - * This method will be used to iterate through all the controls and - * dependents. If found then perform the appropriate action. - */ - var dependentsFound = function(pref_name, pref_val, dependents) { - // Iterate through all the controls and check the dependents - _.each(controls, function(c) { - let ctrl_name = c.model.get('name'); - _.each(dependents, function(deps) { - if (ctrl_name === deps) { - // Create methods to take appropriate actions and call here. - enableDisableMaxWidth(pref_name, pref_val, c); - } - }); - }); - }; - - /* - * Function: enableDisableMaxWidth - * - * This method will be used to enable and disable Maximum Width control - */ - var enableDisableMaxWidth = function(pref_name, pref_val, control) { - if (pref_name === 'column_data_auto_resize' && pref_val === 'by_name') { - control.$el.find('input').prop('disabled', true); - control.$el.find('input').val(0); - } else if (pref_name === 'column_data_auto_resize' && pref_val === 'by_data') { - control.$el.find('input').prop('disabled', false); - } - }; - - /* - * Function: renderPreferencePanel - * - * Renders the preference panel in the content div based on the given - * preferences. - */ - var renderPreferencePanel = function(prefs) { - /* - * Clear the existing html in the preferences content - */ - var content = $container.find('.preferences_content'); - - /* - * We should clean up the existing controls. - */ - if (controls) { - _.each(controls, function(c) { - if ('$sel' in c) { - if (c.$sel.data('select2').isOpen()) c.$sel.data('select2').close(); - } - c.remove(); - }); - } - content.empty(); - controls = []; - - /* - * We will create new set of controls and render it based on the - * list of preferences using the Backform Field, Control. - */ - _.each(prefs, function(p) { - - var m = preferences.get(p.id); - m.errorModel = new Backbone.Model(); - var f = new Backform.Field( - _.extend({}, p, { - id: 'value', - name: 'value', - }) - ), - cntr = new(f.get('control'))({ - field: f, - model: m, - }); - content.append(cntr.render().$el); - - // We will keep track of all the controls rendered at the - // moment. - controls.push(cntr); - }); - - /* Iterate through all preferences and check if dependents found. - * If found then call the dependentsFound method - */ - _.each(prefs, function(p) { - let m = preferences.get(p.id); - let dependents = m.get('dependents'); - if (!_.isNull(dependents) && Array.isArray(dependents) && dependents.length > 0) { - dependentsFound(m.get('name'), m.get('value'), dependents); - } - }); - }; - - /* - * Function: dialogContentCleanup - * - * Do the dialog container cleanup on openning. - */ - - var dialogContentCleanup = function() { - // Remove the existing preferences - if (!jTree) - return; - - /* - * Remove the aci-tree (mainly to remove the jquery object of - * aciTree from the system for this container). - */ - try { - jTree.aciTree('destroy'); - } catch (ex) { - // Sometimes - it fails to destroy the tree properly and throws - // exception. - console.warn(ex.stack || ex); - } - jTree.off('acitree', treeEventHandler); - - // We need to reset the data from the preferences too - preferences.reset(); - - /* - * Clean up the existing controls. - */ - if (controls) { - _.each(controls, function(c) { - c.remove(); - }); - } - controls = []; - - // Remove all the objects now. - $container.empty(); - }, - /* - * Function: selectFirstCategory - * - * Whenever a user select a module instead of a category, we should - * select the first categroy of it. - */ - selectFirstCategory = function(api, item) { - var data = item ? api.itemData(item) : null; - - if (data && data.preferences) { - api.select(item); - return; - } - item = api.first(item); - selectFirstCategory(api, item); - }, - /* - * A map on how to create controls for each datatype in preferences - * dialog. - */ - getControlMappedForType = function(p) { - switch (p.type) { - case 'text': - return 'input'; - case 'boolean': - p.options = { - onText: gettext('True'), - offText: gettext('False'), - onColor: 'success', - offColor: 'ternary', - size: 'mini', - }; - return 'switch'; - case 'node': - p.options = { - onText: gettext('Show'), - offText: gettext('Hide'), - onColor: 'success', - offColor: 'ternary', - size: 'mini', - width: '56', - }; - return 'switch'; - case 'integer': - return 'numeric'; - case 'numeric': - return 'numeric'; - case 'date': - return 'datepicker'; - case 'datetime': - return 'datetimepicker'; - case 'options': - var opts = [], - has_value = false; - // Convert the array to SelectControl understandable options. - _.each(p.options, function(o) { - if ('label' in o && 'value' in o) { - let push_var = { - 'label': o.label, - 'value': o.value, - }; - push_var['label'] = o.label; - push_var['value'] = o.value; - - if('preview_src' in o) { - push_var['preview_src'] = o.preview_src; - } - opts.push(push_var); - if (o.value == p.value) - has_value = true; - } else { - opts.push({ - 'label': o, - 'value': o, - }); - if (o == p.value) - has_value = true; - } - }); - if (p.select2 && p.select2.tags == true && p.value && has_value == false) { - opts.push({ - 'label': p.value, - 'value': p.value, - }); - } - p.options = opts; - return 'select2'; - case 'select2': - var select_opts = []; - _.each(p.options, function(o) { - if ('label' in o && 'value' in o) { - let push_var = { - 'label': o.label, - 'value': o.value, - }; - push_var['label'] = o.label; - push_var['value'] = o.value; - - if('preview_src' in o) { - push_var['preview_src'] = o.preview_src; - } - select_opts.push(push_var); - } else { - select_opts.push({ - 'label': o, - 'value': o, - }); - } - }); - - p.options = select_opts; - return 'select2'; - - case 'multiline': - return 'textarea'; - case 'switch': - return 'switch'; - case 'keyboardshortcut': - return 'keyboardShortcut'; - case 'radioModern': - return 'radioModern'; - case 'selectFile': - return 'binary-paths-grid'; - case 'threshold': - p.warning_label = gettext('Warning'); - p.alert_label = gettext('Alert'); - p.unit = gettext('(in minutes)'); - return 'threshold'; - default: - if (console && console.warn) { - // Warning for developer only. - console.warn( - 'Hmm.. We don\'t know how to render this type - \'\'' + p.type + '\' of control.' - ); - } - return 'input'; - } - }, - /* - * function: treeEventHandler - * - * It is basically a callback, which listens to aci-tree events, - * and act accordingly. - * - * + Selection of the node will existance of the preferences for - * the selected tree-node, if not pass on to select the first - * category under a module, else pass on to the render function. - * - * + When a new node is added in the tree, it will add the relavent - * preferences in the preferences model collection, which will be - * called during initialization itself. - * - * - */ - treeEventHandler = function(event, api, item, eventName) { - // Look for selected item (if none supplied)! - item = item || api.selected(); - - // Event tree item has itemData - var d = item ? api.itemData(item) : null; - - /* - * boolean (switch/checkbox), string, enum (combobox - enumvals), - * integer (min-max), font, color - */ - switch (eventName) { - case 'selected': - if (!d) - break; - - if (d.preferences) { - /* - * Clear the existing html in the preferences content - */ - $container.find('.preferences_content'); - - renderPreferencePanel(d.preferences); - - break; - } else { - selectFirstCategory(api, item); - } - break; - case 'added': - if (!d) - break; - - // We will add the preferences in to the preferences data - // collection. - if (d.preferences && _.isArray(d.preferences)) { - _.each(d.preferences, function(p) { - preferences.add({ - 'id': p.id, - 'value': p.value, - 'category_id': d.id, - 'mid': d.mid, - 'name': p.name, - 'dependents': p.dependents, - }); - /* - * We don't know until now, how to render the control for - * this preference. - */ - if (!p.control) { - p.control = getControlMappedForType(p); - } - if (p.help_str) { - p.helpMessage = p.help_str; - } - }); - } - d.sortable = false; - break; - case 'loaded': - // Let's select the first category from the prefrences. - // We need to wait for sometime before all item gets loaded - // properly. - setTimeout( - function() { - selectFirstCategory(api, null); - }, 300); - break; - } - return true; - }; - - // Dialog property - return { - main: function() { - - // Remove the existing content first. - dialogContentCleanup(); - - $container.append( - '
' - ).append( - '
' + - gettext('Category is not selected.') + - '
' - ); - - // Create the aci-tree for listing the modules and categories of - // it. - jTree = $container.find('.preferences_tree'); - jTree.on('acitree', treeEventHandler); - - jTree.aciTree({ - selectable: true, - expand: true, - fullRow: true, - ajax: { - url: url_for('preferences.index'), - }, - animateRoot: true, - unanimated: false, - show: {duration: 75}, - hide: {duration: 75}, - view: {duration: 75}, - }); - - if (jTree.aciTree('api')) modifyAnimation.modifyAcitreeAnimation(pgBrowser, jTree.aciTree('api')); - - this.show(); - }, - setup: function() { - return { - buttons: [{ - text: '', - key: 112, - className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button', - attrs: { - name: 'dialog_help', - type: 'button', - label: gettext('Preferences'), - 'aria-label': gettext('Help'), - url: url_for( - 'help.static', { - 'filename': 'preferences.html', - } - ), - }, - }, { - text: gettext('Cancel'), - key: 27, - className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button', - }, { - text: gettext('Save'), - key: 13, - className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button', - }], - focus: { - element: 0, - }, - options: { - padding: !1, - overflow: !1, - title: gettext('Preferences'), - closableByDimmer: false, - modal: true, - pinnable: false, - }, - }; - }, - callback: function(e) { - if (e.button.element.name == 'dialog_help') { - e.cancel = true; - pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'), - null, null); - return; - } - - if (e.button.text == gettext('Save')) { - let requires_refresh = false; - preferences.updateAll(); - - /* Find the modules changed */ - let modulesChanged = {}; - _.each(changed, (val, key)=> { - let pref = pgBrowser.get_preference_for_id(Number(key)); - - if(pref['name'] == 'dynamic_tabs') { - showQueryTool._set_dynamic_tab(pgBrowser, !pref['value']); - } - - if(!modulesChanged[pref.module]) { - modulesChanged[pref.module] = true; - } - - if(pref.name == 'theme') { - requires_refresh = true; - } - - if(pref.name == 'hide_shared_server') { - Notify.confirm( - gettext('Browser tree refresh required'), - gettext('A browser tree refresh is required. Do you wish to refresh the tree?'), - function() { - pgAdmin.Browser.tree.destroy({ - success: function() { - pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser); - return true; - }, - }); - }, - function() { - preferences.reset(); - changed = {}; - return true; - }, - gettext('Refresh'), - gettext('Later') - ); - } - }); - - if(requires_refresh) { - Notify.confirm( - gettext('Refresh required'), - gettext('A page refresh is required to apply the theme. Do you wish to refresh the page now?'), - function() { - /* If user clicks Yes */ - location.reload(); - return true; - }, - function() {/* If user clicks No */ return true;}, - gettext('Refresh'), - gettext('Later') - ); - } - // Refresh preferences cache - pgBrowser.cache_preferences(modulesChanged); - } - }, - build: function() { - this.elements.content.appendChild($container.get(0)); - Alertify.pgDialogBuild.apply(this); - }, - hooks: { - onshow: function() {/* This is intentional (SonarQube) */}, - }, - }; - }); - - }, - show: function() { - Alertify.preferencesDlg(true).resizeTo(pgAdmin.Browser.stdW.calc(pgAdmin.Browser.stdW.lg),pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg)); - }, - }; - - return pgAdmin.Preferences; -}); + // This is a callback function to show preferences. + show() { + // Render Preferences component + Notify.showModal(gettext('Preferences'), (closeModal) => { + return { + initPreferencesTree(this.pgBrowser, document.getElementById('treeContainer'), prefTreeData); + }} closeModal={closeModal} />; + }, { isFullScreen: false, isResizeable: true, showFullScreen: true, isFullWidth: true, dialogWidth: 900, dialogHeight: 550 }); + } +} diff --git a/web/pgadmin/preferences/tests/__init__.py b/web/pgadmin/preferences/tests/__init__.py new file mode 100644 index 000000000..6e04daf61 --- /dev/null +++ b/web/pgadmin/preferences/tests/__init__.py @@ -0,0 +1,8 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2022, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## diff --git a/web/pgadmin/preferences/tests/preferences_test_data.json b/web/pgadmin/preferences/tests/preferences_test_data.json new file mode 100644 index 000000000..a4327dd4d --- /dev/null +++ b/web/pgadmin/preferences/tests/preferences_test_data.json @@ -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 + } + } + ] +} diff --git a/web/pgadmin/preferences/tests/test_preferences_get.py b/web/pgadmin/preferences/tests/test_preferences_get.py new file mode 100644 index 000000000..b78638886 --- /dev/null +++ b/web/pgadmin/preferences/tests/test_preferences_get.py @@ -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) diff --git a/web/pgadmin/preferences/tests/test_preferences_update.py b/web/pgadmin/preferences/tests/test_preferences_update.py new file mode 100644 index 000000000..6b9eccc0c --- /dev/null +++ b/web/pgadmin/preferences/tests/test_preferences_update.py @@ -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') diff --git a/web/pgadmin/static/img/fonticon/expand.svg b/web/pgadmin/static/img/fonticon/expand.svg new file mode 100644 index 000000000..552bd9d17 --- /dev/null +++ b/web/pgadmin/static/img/fonticon/expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/pgadmin/static/img/fonticon/minimize_collapse.svg b/web/pgadmin/static/img/fonticon/minimize_collapse.svg new file mode 100644 index 000000000..439505640 --- /dev/null +++ b/web/pgadmin/static/img/fonticon/minimize_collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx index 31d5e810f..914ac5dd3 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView.jsx @@ -359,7 +359,7 @@ export default function DataGridView({ /* Make sure to take the latest field info from schema */ field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field; - let {editable} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps); + let {editable, disabled} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps); if(_.isUndefined(field.cell)) { console.error('cell is required ', field); @@ -368,9 +368,17 @@ export default function DataGridView({ return { + if(field.radioType) { + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.BULK_UPDATE, + path: accessPath, + value: changeValue, + id: field.id + }); + } dataDispatch({ type: SCHEMA_STATE_ACTIONS.SET_VALUE, path: accessPath.concat([row.index, field.id]), diff --git a/web/pgadmin/static/js/SchemaView/MappedControl.jsx b/web/pgadmin/static/js/SchemaView/MappedControl.jsx index a361b8286..31a5e79a0 100644 --- a/web/pgadmin/static/js/SchemaView/MappedControl.jsx +++ b/web/pgadmin/static/js/SchemaView/MappedControl.jsx @@ -9,18 +9,19 @@ import React, { useCallback } from 'react'; import _ from 'lodash'; - -import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor, - FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, FormNote, FormInputDateTimePicker, PlainString, InputSQL, - InputSelect, InputText, InputCheckbox, InputDateTimePicker } from '../components/FormComponents'; +import { + FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor, + FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString, + InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputThemes, InputRadio +} from '../components/FormComponents'; import Privilege from '../components/Privilege'; import { evalFunc } from 'sources/utils'; import PropTypes from 'prop-types'; import CustomPropTypes from '../custom_prop_types'; -import { SelectRefresh} from '../components/SelectRefresh'; +import { SelectRefresh } from '../components/SelectRefresh'; /* Control mapping for form view */ -function MappedFormControlBase({type, value, id, onChange, className, visible, inputRef, noLabel, ...props}) { +function MappedFormControlBase({ type, value, id, onChange, className, visible, inputRef, noLabel, ...props }) { const name = id; const onTextChange = useCallback((e) => { let val = e; @@ -34,36 +35,36 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i onChange && onChange(changedValue); }, []); - if(!visible) { + if (!visible) { return <>; } /* The mapping uses Form* components as it comes with labels */ switch (type) { case 'int': - return ; + return ; case 'numeric': - return ; + return ; case 'tel': - return ; + return ; case 'text': - return ; + return ; case 'multiline': return ; + inputRef={inputRef} controlProps={{ multiline: true }} {...props} />; case 'password': - return ; + return ; case 'select': return ; case 'select-refresh': return ; case 'switch': return onTextChange(e.target.checked, e.target.name)} className={className} + onChange={(e) => onTextChange(e.target.checked, e.target.name)} className={className} {...props} />; case 'checkbox': return onTextChange(e.target.checked, e.target.name)} className={className} + onChange={(e) => onTextChange(e.target.checked, e.target.name)} className={className} {...props} />; case 'toggle': return ; case 'note': - return ; + return ; case 'datetimepicker': return ; + case 'keyboardShortcut': + return ; + case 'threshold': + return ; + case 'theme': + return ; default: return ; } @@ -100,17 +107,25 @@ MappedFormControlBase.propTypes = { }; /* Control mapping for grid cell view */ -function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow,...props}) { +function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, ...props }) { const name = id; const onTextChange = useCallback((e) => { let val = e; - if(e && e.target) { + if (e && e.target) { val = e.target.value; } onCellChange && onCellChange(val); }, []); + const onRadioChange = useCallback((e) => { + let val =e; + if(e && e.target) { + val = e.target.checked; + } + onCellChange && onCellChange(val); + }); + const onSqlChange = useCallback((val) => { onCellChange && onCellChange(val); }, []); @@ -118,13 +133,13 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi /* Some grid cells are based on options selected in other cells. * lets trigger a re-render for the row if optionsLoaded */ - const optionsLoadedRerender = useCallback((res)=>{ + const optionsLoadedRerender = useCallback((res) => { /* optionsLoaded is called when select options are fetched */ optionsLoaded && optionsLoaded(res); reRenderRow && reRenderRow(); }, []); - if(!visible) { + if (!visible) { return <>; } @@ -152,6 +167,12 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi return ; case 'sql': return ; + case 'file': + return ; + case 'keyCode': + return ; + case 'radio': + return ; default: return ; } @@ -167,14 +188,16 @@ MappedCellControlBase.propTypes = { reRenderRow: PropTypes.func, optionsLoaded: PropTypes.func, onCellChange: PropTypes.func, - visible: PropTypes.bool + visible: PropTypes.bool, + disabled: PropTypes.bool, + inputRef: CustomPropTypes.ref, }; const ALLOWED_PROPS_FIELD_COMMON = [ 'mode', 'value', 'readonly', 'disabled', 'hasError', 'id', 'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef', 'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis', - 'orientation' + 'orientation', 'isvalidate', 'fields', 'radioType' ]; const ALLOWED_PROPS_FIELD_FORM = [ @@ -182,14 +205,14 @@ const ALLOWED_PROPS_FIELD_FORM = [ ]; const ALLOWED_PROPS_FIELD_CELL = [ - 'cell', 'onCellChange', 'row', 'reRenderRow', + 'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType' ]; -export const MappedFormControl = (props)=>{ - let newProps = {...props}; +export const MappedFormControl = (props) => { + let newProps = { ...props }; let typeProps = evalFunc(null, newProps.type, newProps.state); - if(typeof(typeProps) === 'object') { + if (typeof (typeProps) === 'object') { newProps = { ...newProps, ...typeProps, @@ -199,13 +222,13 @@ export const MappedFormControl = (props)=>{ } /* Filter out garbage props if any using ALLOWED_PROPS_FIELD */ - return ; + return ; }; -export const MappedCellControl = (props)=>{ - let newProps = {...props}; +export const MappedCellControl = (props) => { + let newProps = { ...props }; let cellProps = evalFunc(null, newProps.cell, newProps.row); - if(typeof(cellProps) === 'object') { + if (typeof (cellProps) === 'object') { newProps = { ...newProps, ...cellProps, @@ -215,5 +238,5 @@ export const MappedCellControl = (props)=>{ } /* Filter out garbage props if any using ALLOWED_PROPS_FIELD */ - return ; + return ; }; diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 8b20d5f5d..2c180d708 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -108,7 +108,7 @@ function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, i /* The comparator and setter */ const attrChanged = (id, change, force=false)=>{ - if(isValueEqual(_.get(origVal, id), _.get(sessVal, id)) && !force) { + if(isValueEqual(_.get(origVal, id), _.get(sessVal, id)) && !force && (_.isObject(_.get(origVal, id)) && _.isEqual(_.get(origVal, id), _.get(sessData, id)))) { return; } else { change = change || _.get(sessVal, id); @@ -302,6 +302,7 @@ export const SCHEMA_STATE_ACTIONS = { RERENDER: 'rerender', CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue', DEFERRED_DEPCHANGE: 'deferred_depchange', + BULK_UPDATE: 'bulk_update' }; const getDepChange = (currPath, newState, oldState, action)=>{ @@ -354,6 +355,13 @@ const sessDataReducer = (state, action)=>{ case SCHEMA_STATE_ACTIONS.INIT: data = action.payload; break; + case SCHEMA_STATE_ACTIONS.BULK_UPDATE: + rows = (_.get(data, action.path)||[]); + rows.forEach((row)=> { + row[action.id] = false; + }); + _.set(data, action.path, rows); + break; case SCHEMA_STATE_ACTIONS.SET_VALUE: _.set(data, action.path, action.value); /* If there is any dep listeners get the changes */ diff --git a/web/pgadmin/static/js/components/ExternalIcon.jsx b/web/pgadmin/static/js/components/ExternalIcon.jsx index b73a0ecb5..bfb8a2223 100644 --- a/web/pgadmin/static/js/components/ExternalIcon.jsx +++ b/web/pgadmin/static/js/components/ExternalIcon.jsx @@ -11,9 +11,11 @@ import DisconnectedSvg from '../../img/fonticon/disconnected.svg?svgr'; import RegexSvg from '../../img/fonticon/regex.svg?svgr'; import FormatCaseSvg from '../../img/fonticon/format_case.svg?svgr'; import PropTypes from 'prop-types'; +import Expand from '../../img/fonticon/expand.svg?svgr'; +import Collapse from '../../img/fonticon/minimize_collapse.svg?svgr'; export default function ExternalIcon({Icon, ...props}) { - return ; + return ; } ExternalIcon.propTypes = { @@ -31,4 +33,6 @@ export const ConnectedIcon = ()=>; export const RegexIcon = ()=>; export const FormatCaseIcon = ()=>; +export const ExpandDialog = ()=>; +export const MinimizeDialog = ()=>; diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index cdcfab643..0f8ed3623 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -10,8 +10,10 @@ import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { Box, FormControl, OutlinedInput, FormHelperText, - Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect } from '@material-ui/core'; +import { + Box, FormControl, OutlinedInput, FormHelperText, + Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect, Radio, +} from '@material-ui/core'; import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab'; import ErrorRoundedIcon from '@material-ui/icons/ErrorOutlineRounded'; import InfoRoundedIcon from '@material-ui/icons/InfoRounded'; @@ -20,13 +22,14 @@ import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; import WarningRoundedIcon from '@material-ui/icons/WarningRounded'; import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded'; import DescriptionIcon from '@material-ui/icons/Description'; -import Select, {components as RSComponents} from 'react-select'; +import AssignmentTurnedIn from '@material-ui/icons/AssignmentTurnedIn'; +import Select, { components as RSComponents } from 'react-select'; import CreatableSelect from 'react-select/creatable'; import Pickr from '@simonwep/pickr'; import clsx from 'clsx'; import PropTypes from 'prop-types'; import HTMLReactParse from 'html-react-parser'; -import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider} from '@material-ui/pickers'; +import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; import DateFnsUtils from '@date-io/date-fns'; import * as DateFns from 'date-fns'; @@ -36,6 +39,9 @@ import { showFileDialog } from '../helpers/legacyConnector'; import _ from 'lodash'; import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons'; import CustomPropTypes from '../custom_prop_types'; +import KeyboardShortcuts from './KeyboardShortcuts'; +import QueryThresholds from './QueryThresholds'; +import Themes from './Themes'; const useStyles = makeStyles((theme) => ({ @@ -55,7 +61,7 @@ const useStyles = makeStyles((theme) => ({ margin: theme.spacing(0.75, 0.75, 0.75, 0.75), display: 'flex', }, - formLabelError: { + formLabelError: { color: theme.palette.error.main, }, sql: { @@ -95,17 +101,17 @@ export const MESSAGE_TYPE = { }; /* Icon based on MESSAGE_TYPE */ -function FormIcon({type, close=false, ...props}) { +function FormIcon({ type, close = false, ...props }) { let TheIcon = null; - if(close) { + if (close) { TheIcon = CloseIcon; - } else if(type === MESSAGE_TYPE.SUCCESS) { + } else if (type === MESSAGE_TYPE.SUCCESS) { TheIcon = CheckRoundedIcon; - } else if(type === MESSAGE_TYPE.ERROR) { + } else if (type === MESSAGE_TYPE.ERROR) { TheIcon = ErrorRoundedIcon; - } else if(type === MESSAGE_TYPE.INFO) { + } else if (type === MESSAGE_TYPE.INFO) { TheIcon = InfoRoundedIcon; - } else if(type === MESSAGE_TYPE.WARNING) { + } else if (type === MESSAGE_TYPE.WARNING) { TheIcon = WarningRoundedIcon; } @@ -117,21 +123,21 @@ FormIcon.propTypes = { }; /* Wrapper on any form component to add label, error indicator and help message */ -export function FormInput({children, error, className, label, helpMessage, required, testcid}) { +export function FormInput({ children, error, className, label, helpMessage, required, testcid }) { const classes = useStyles(); const cid = testcid || _.uniqueId('c'); const helpid = `h${cid}`; return ( - + {label} - + - {React.cloneElement(children, {cid, helpid})} + {React.cloneElement(children, { cid, helpid })} {HTMLReactParse(helpMessage || '')} @@ -148,17 +154,22 @@ FormInput.propTypes = { testcid: PropTypes.any, }; -export function InputSQL({value, onChange, className, controlProps, ...props}) { +export function InputSQL({ value, options, onChange, className, controlProps, ...props }) { const classes = useStyles(); const editor = useRef(); return ( editor.current=obj} - value={value||''} + currEditor={(obj) => editor.current = obj} + value={value || ''} + options={{ + lineNumbers: true, + mode: 'text/x-pgsql', + ...options, + }} className={clsx(classes.sql, className)} events={{ - change: (cm)=>{ + change: (cm) => { onChange && onChange(cm.getValue()); }, }} @@ -176,13 +187,13 @@ InputSQL.propTypes = { controlProps: PropTypes.object, }; -export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, noLabel, ...props}) { - if(noLabel) { - return ; +export function FormInputSQL({ hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props }) { + if (noLabel) { + return ; } else { return ( - + ); } @@ -208,7 +219,7 @@ const DATE_TIME_FORMAT = { TIME_24: 'HH:mm:ss', }; -export function InputDateTimePicker({value, onChange, readonly, controlProps, ...props}) { +export function InputDateTimePicker({ value, onChange, readonly, controlProps, ...props }) { let format = ''; let placeholder = ''; if (controlProps?.pickerType === 'Date') { @@ -222,15 +233,15 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, .. placeholder = controlProps.placeholder || 'YYYY-MM-DD HH:mm:ss Z'; } - const handleChange = (dateVal, stringVal)=> { + const handleChange = (dateVal, stringVal) => { onChange(stringVal); }; /* Value should be a date object instead of string */ value = _.isUndefined(value) ? null : value; - if(!_.isNull(value)) { + if (!_.isNull(value)) { let parseValue = DateFns.parse(value, format, new Date()); - if(!DateFns.isValid(parseValue)) { + if (!DateFns.isValid(parseValue)) { parseValue = DateFns.parseISO(value); } value = !DateFns.isValid(parseValue) ? value : parseValue; @@ -238,7 +249,7 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, .. if (readonly) { return (); + readonly={readonly} controlProps={{ placeholder: controlProps.placeholder }} {...props} />); } let commonProps = { @@ -262,20 +273,20 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, .. if (controlProps?.pickerType === 'Date') { return ( - + ); } else if (controlProps?.pickerType === 'Time') { return ( - + ); } return ( - + ); } @@ -287,10 +298,10 @@ InputDateTimePicker.propTypes = { controlProps: PropTypes.object, }; -export function FormInputDateTimePicker({hasError, required, label, className, helpMessage, testcid, ...props}) { +export function FormInputDateTimePicker({ hasError, required, label, className, helpMessage, testcid, ...props }) { return ( - + ); } @@ -308,23 +319,23 @@ FormInputDateTimePicker.propTypes = { /* Use forwardRef to pass ref prop to OutlinedInput */ export const InputText = forwardRef(({ - cid, helpid, readonly, disabled, maxlength=255, value, onChange, controlProps, type, ...props}, ref)=>{ + cid, helpid, readonly, disabled, maxlength = 255, value, onChange, controlProps, type, ...props }, ref) => { const classes = useStyles(); const patterns = { 'numeric': '^-?[0-9]\\d*\\.?\\d*$', 'int': '^-?[0-9]\\d*$', }; - let onChangeFinal = (e)=>{ + let onChangeFinal = (e) => { let changeVal = e.target.value; /* For type number, we set type as tel with number regex to get validity.*/ - if(['numeric', 'int', 'tel'].indexOf(type) > -1) { - if(!e.target.validity.valid && changeVal !== '' && changeVal !== '-') { + if (['numeric', 'int', 'tel'].indexOf(type) > -1) { + if (!e.target.validity.valid && changeVal !== '' && changeVal !== '-') { return; } } - if(controlProps?.formatter) { + if (controlProps?.formatter) { changeVal = controlProps.formatter.toRaw(changeVal); } onChange && onChange(changeVal); @@ -332,11 +343,11 @@ export const InputText = forwardRef(({ let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value; - if(controlProps?.formatter) { + if (controlProps?.formatter) { finalValue = controlProps.formatter.fromRaw(finalValue); } - return( + return ( -1 ? {type: 'tel'} : {type: type})} + {...(['numeric', 'int'].indexOf(type) > -1 ? { type: 'tel' } : { type: type })} /> ); }); @@ -374,10 +388,10 @@ InputText.propTypes = { type: PropTypes.string, }; -export function FormInputText({hasError, required, label, className, helpMessage, testcid, ...props}) { +export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) { return ( - + ); } @@ -391,16 +405,21 @@ FormInputText.propTypes = { }; /* Using the existing file dialog functions using showFileDialog */ -export function InputFileSelect({controlProps, onChange, disabled, readonly, ...props}) { +export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, validate, ...props }) { const inpRef = useRef(); - const onFileSelect = (value)=>{ + const onFileSelect = (value) => { onChange && onChange(decodeURI(value)); inpRef.current.focus(); }; return ( showFileDialog(controlProps, onFileSelect)} - disabled={disabled||readonly} aria-label={gettext('Select a file')}> + <> + showFileDialog(controlProps, onFileSelect)} + disabled={disabled || readonly} aria-label={gettext('Select a file')}> + {isvalidate && + { validate(props.value); }} icon={}> + } + } /> ); } @@ -409,14 +428,17 @@ InputFileSelect.propTypes = { onChange: PropTypes.func, disabled: PropTypes.bool, readonly: PropTypes.bool, + isvalidate: PropTypes.bool, + validate: PropTypes.func, + value: PropTypes.string }; export function FormInputFileSelect({ - hasError, required, label, className, helpMessage, testcid, ...props}) { + hasError, required, label, className, helpMessage, testcid, ...props }) { return ( - + ); } @@ -429,13 +451,13 @@ FormInputFileSelect.propTypes = { testcid: PropTypes.string, }; -export function InputSwitch({cid, helpid, value, onChange, readonly, controlProps, ...props}) { +export function InputSwitch({ cid, helpid, value, onChange, readonly, controlProps, ...props }) { const classes = useStyles(); return ( {/*This is intentional (SonarQube)*/} : onChange + readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange } id={cid} inputProps={{ @@ -457,11 +479,11 @@ InputSwitch.propTypes = { controlProps: PropTypes.object, }; -export function FormInputSwitch({hasError, required, label, className, helpMessage, testcid, ...props}) { +export function FormInputSwitch({ hasError, required, label, className, helpMessage, testcid, ...props }) { return ( - + ); } @@ -474,7 +496,7 @@ FormInputSwitch.propTypes = { testcid: PropTypes.string, }; -export function InputCheckbox({cid, helpid, value, onChange, controlProps, readonly, ...props}) { +export function InputCheckbox({ cid, helpid, value, onChange, controlProps, readonly, ...props }) { controlProps = controlProps || {}; return ( {/*This is intentional (SonarQube)*/} : onChange} + onChange={readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange} color="primary" - inputProps={{'aria-describedby': helpid}} - {...props}/> + inputProps={{ 'aria-describedby': helpid }} + {...props} /> } label={controlProps.label} + labelPlacement={props?.labelPlacement ? props.labelPlacement : 'end'} /> ); } @@ -498,14 +521,15 @@ InputCheckbox.propTypes = { controlProps: PropTypes.object, onChange: PropTypes.func, readonly: PropTypes.bool, + labelPlacement: PropTypes.string }; -export function FormInputCheckbox({hasError, required, label, - className, helpMessage, testcid, ...props}) { +export function FormInputCheckbox({ hasError, required, label, + className, helpMessage, testcid, ...props }) { return ( - + ); } @@ -518,24 +542,61 @@ FormInputCheckbox.propTypes = { testcid: PropTypes.string, }; +export function InputRadio({ helpid, value, onChange, controlProps, readonly, ...props }) { + const classes = useStyles(); + controlProps = controlProps || {}; + return ( + { + /*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 ( {val!==null && onChange(val);}} + onChange={(e, val) => { val !== null && onChange(val); }} {...props} > { - (options||[]).map((option, i)=>{ + (options || []).map((option, i) => { const isSelected = option.value === value; const isDisabled = disabled || option.disabled || (readonly && !isSelected); return ( - -  {option.label} +  {option.label} ); }) @@ -554,11 +615,11 @@ InputToggle.propTypes = { readonly: PropTypes.bool, }; -export function FormInputToggle({hasError, required, label, - className, helpMessage, testcid, inputRef, ...props}) { +export function FormInputToggle({ hasError, required, label, + className, helpMessage, testcid, inputRef, ...props }) { return ( - + ); } @@ -575,9 +636,9 @@ FormInputToggle.propTypes = { /* react-select package is used for select input * Customizing the select styles to fit existing theme */ -const customReactSelectStyles = (theme, readonly)=>({ +const customReactSelectStyles = (theme, readonly) => ({ input: (provided) => { - return {...provided, padding: 0, margin: 0, color: 'inherit'}; + return { ...provided, padding: 0, margin: 0, color: 'inherit' }; }, singleValue: (provided) => { return { @@ -593,35 +654,35 @@ const customReactSelectStyles = (theme, readonly)=>({ borderColor: theme.otherVars.inputBorderColor, ...(state.isFocused ? { borderColor: theme.palette.primary.main, - boxShadow: 'inset 0 0 0 1px '+theme.palette.primary.main, + boxShadow: 'inset 0 0 0 1px ' + theme.palette.primary.main, '&:hover': { borderColor: theme.palette.primary.main, } } : {}), }), - dropdownIndicator: (provided)=>({ + dropdownIndicator: (provided) => ({ ...provided, padding: '0rem 0.25rem', }), - indicatorsContainer: (provided)=>({ + indicatorsContainer: (provided) => ({ ...provided, - ...(readonly ? {display: 'none'} : {}) + ...(readonly ? { display: 'none' } : {}) }), - clearIndicator: (provided)=>({ + clearIndicator: (provided) => ({ ...provided, padding: '0rem 0.25rem', }), - valueContainer: (provided)=>({ + valueContainer: (provided) => ({ ...provided, padding: theme.otherVars.reactSelect.padding, }), - groupHeading: (provided)=>({ + groupHeading: (provided) => ({ ...provided, color: 'inherit', fontSize: '0.85em', textTransform: 'none', }), - menu: (provided)=>({ + menu: (provided) => ({ ...provided, backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, @@ -629,12 +690,12 @@ const customReactSelectStyles = (theme, readonly)=>({ border: '1px solid ' + theme.otherVars.inputBorderColor, marginTop: '2px', }), - menuPortal: (provided)=>({ + menuPortal: (provided) => ({ ...provided, zIndex: 9999, backgroundColor: 'inherit', color: 'inherit', }), - option: (provided, state)=>{ + option: (provided, state) => { let bgColor = 'inherit'; if (state.isFocused) { bgColor = theme.palette.grey[400]; @@ -648,27 +709,27 @@ const customReactSelectStyles = (theme, readonly)=>({ backgroundColor: bgColor, }; }, - multiValue: (provided)=>({ + multiValue: (provided) => ({ ...provided, backgroundColor: theme.palette.grey[400], }), - multiValueLabel: (provided)=>({ + multiValueLabel: (provided) => ({ ...provided, fontSize: '1em', zIndex: 99, color: theme.palette.text.primary }), - multiValueRemove: (provided)=>({ + multiValueRemove: (provided) => ({ ...provided, '&:hover': { backgroundColor: 'unset', color: theme.palette.error.main, }, - ...(readonly ? {display: 'none'} : {}) + ...(readonly ? { display: 'none' } : {}) }), }); -function OptionView({image, label}) { +function OptionView({ image, label }) { const classes = useStyles(); return ( <> @@ -705,8 +766,8 @@ CustomSelectSingleValue.propTypes = { }; export function flattenSelectOptions(options) { - return _.flatMap(options, (option)=>{ - if(option.options) { + return _.flatMap(options, (option) => { + if (option.options) { return option.options; } else { return option; @@ -716,28 +777,28 @@ export function flattenSelectOptions(options) { function getRealValue(options, value, creatable, formatter) { let realValue = null; - if(_.isArray(value)) { + if (_.isArray(value)) { realValue = [...value]; /* If multi select options need to be in some format by UI, use formatter */ - if(formatter) { + if (formatter) { realValue = formatter.fromRaw(realValue, options); } else { - if(creatable) { - realValue = realValue.map((val)=>({label:val, value: val})); + if (creatable) { + realValue = realValue.map((val) => ({ label: val, value: val })); } else { - realValue = realValue.map((val)=>(_.find(options, (option)=>_.isEqual(option.value, val)))); + realValue = realValue.map((val) => (_.find(options, (option) => _.isEqual(option.value, val)))); } } } else { let flatOptions = flattenSelectOptions(options); - realValue = _.find(flatOptions, (option)=>option.value==value) || - (creatable && !_.isUndefined(value) && !_.isNull(value) ? {label:value, value: value} : null); + realValue = _.find(flatOptions, (option) => option.value == value) || + (creatable && !_.isUndefined(value) && !_.isNull(value) ? { label: value, value: value } : null); } return realValue; } -export function InputSelectNonSearch({options, ...props}) { +export function InputSelectNonSearch({ options, ...props }) { return - {(options||[]).map((o)=>)} + {(options || []).map((o) => )} ; } InputSelectNonSearch.propTypes = { @@ -748,7 +809,7 @@ InputSelectNonSearch.propTypes = { }; export const InputSelect = forwardRef(({ - cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, optionsReloadBasis, disabled, ...props}, ref) => { + cid, onChange, options, readonly = false, value, controlProps = {}, optionsLoaded, optionsReloadBasis, disabled, ...props }, ref) => { const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]); const theme = useTheme(); @@ -757,33 +818,33 @@ export const InputSelect = forwardRef(({ loading the options. optionsReloadBasis is helpful to avoid repeated options load. If optionsReloadBasis value changes, then options will be loaded again. */ - useEffect(()=>{ - let optPromise = options, umounted=false; - if(typeof options === 'function') { + useEffect(() => { + let optPromise = options, umounted = false; + if (typeof options === 'function') { optPromise = options(); } setFinalOptions([[], true]); Promise.resolve(optPromise) - .then((res)=>{ + .then((res) => { /* If component unmounted, dont update state */ - if(!umounted) { + if (!umounted) { optionsLoaded && optionsLoaded(res, value); /* Auto select if any option has key as selected */ const flatRes = flattenSelectOptions(res || []); let selectedVal; - if(controlProps.multiple) { - selectedVal = _.filter(flatRes, (o)=>o.selected)?.map((o)=>o.value); + if (controlProps.multiple) { + selectedVal = _.filter(flatRes, (o) => o.selected)?.map((o) => o.value); } else { - selectedVal = _.find(flatRes, (o)=>o.selected)?.value; + selectedVal = _.find(flatRes, (o) => o.selected)?.value; } - if((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) { + if ((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) { onChange && onChange(selectedVal); } setFinalOptions([res || [], false]); } }); - return ()=>umounted=true; + return () => umounted = true; }, [optionsReloadBasis]); @@ -791,7 +852,7 @@ export const InputSelect = forwardRef(({ const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions; const flatFiltered = flattenSelectOptions(filteredOptions); let realValue = getRealValue(flatFiltered, value, controlProps.creatable, controlProps.formatter); - if(realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) { + if (realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) { console.error('Undefined option value not allowed', realValue, filteredOptions); } const otherProps = { @@ -802,17 +863,17 @@ export const InputSelect = forwardRef(({ const styles = customReactSelectStyles(theme, readonly || disabled); - const onChangeOption = useCallback((selectVal)=>{ - if(_.isArray(selectVal)) { + const onChangeOption = useCallback((selectVal) => { + if (_.isArray(selectVal)) { // Check if select all option is selected if (!_.isUndefined(selectVal.find(x => x.label === 'Select All'))) { selectVal = filteredOptions; } /* If multi select options need to be in some format by UI, use formatter */ - if(controlProps.formatter) { + if (controlProps.formatter) { selectVal = controlProps.formatter.toRaw(selectVal, filteredOptions); } else { - selectVal = selectVal.map((option)=>option.value); + selectVal = selectVal.map((option) => option.value); } onChange && onChange(selectVal); } else { @@ -838,13 +899,13 @@ export const InputSelect = forwardRef(({ ...otherProps, ...props, }; - if(!controlProps.creatable) { + if (!controlProps.creatable) { return ( - ); } else { return ( - + ); } }); @@ -863,10 +924,10 @@ InputSelect.propTypes = { export function FormInputSelect({ - hasError, required, className, label, helpMessage, testcid, ...props}) { + hasError, required, className, label, helpMessage, testcid, ...props }) { return ( - + ); } @@ -881,7 +942,7 @@ FormInputSelect.propTypes = { }; /* React wrapper on color pickr */ -export function InputColor({value, controlProps, disabled, onChange, currObj}) { +export function InputColor({ value, controlProps, disabled, onChange, currObj }) { const pickrOptions = { showPalette: true, allowEmpty: true, @@ -896,19 +957,19 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) { const pickrObj = useRef(); const classes = useStyles(); - const setColor = (newVal)=>{ + const setColor = (newVal) => { pickrObj.current && pickrObj.current.setColor((_.isUndefined(newVal) || newVal == '') ? pickrOptions.defaultColor : newVal); }; - const destroyPickr = ()=>{ - if(pickrObj.current) { + const destroyPickr = () => { + if (pickrObj.current) { pickrObj.current.destroy(); pickrObj.current = null; } }; - const initPickr = ()=>{ + const initPickr = () => { /* pickr does not have way to update options, need to destroy and recreate pickr to reflect options */ destroyPickr(); @@ -920,7 +981,7 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) { swatches: [ '#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0', '#f0f', '#f4cccc', '#fce5cd', '#d0e0e3', '#cfe2f3', '#ead1dc', '#ea9999', - '#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666','#93c47d', '#76a5af', '#c27ba0', + '#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666', '#93c47d', '#76a5af', '#c27ba0', '#f1c232', '#6aa84f', '#45818e', '#a64d79', '#bf9000', '#0c343d', '#4c1130', ], position: pickrOptions.position, @@ -941,20 +1002,20 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) { setColor(value); disabled && instance.disable(); - const {lastColor} = instance.getRoot().preview; - const {clear} = instance.getRoot().interaction; + const { lastColor } = instance.getRoot().preview; + const { clear } = instance.getRoot().interaction; /* Cycle the keyboard navigation within the color picker */ - clear.addEventListener('keydown', (e)=>{ - if(e.keyCode === 9) { + clear.addEventListener('keydown', (e) => { + if (e.keyCode === 9) { e.preventDefault(); e.stopPropagation(); lastColor.focus(); } }); - lastColor.addEventListener('keydown', (e)=>{ - if(e.keyCode === 9 && e.shiftKey) { + lastColor.addEventListener('keydown', (e) => { + if (e.keyCode === 9 && e.shiftKey) { e.preventDefault(); e.stopPropagation(); clear.focus(); @@ -965,32 +1026,32 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) { }).on('change', (color) => { onChange && onChange(color.toHEXA().toString()); }).on('show', (color, instance) => { - const {palette} = instance.getRoot().palette; + const { palette } = instance.getRoot().palette; palette.focus(); }).on('hide', (instance) => { const button = instance.getRoot().button; button.focus(); }); - if(currObj) { + if (currObj) { currObj(pickrObj.current); } }; - useEffect(()=>{ + useEffect(() => { initPickr(); - return ()=>{ + return () => { destroyPickr(); }; }, [...Object.values(pickrOptions)]); - useEffect(()=>{ - if(pickrObj.current) { + useEffect(() => { + if (pickrObj.current) { setColor(value); } }, [value]); - let btnStyles = {backgroundColor: value}; + let btnStyles = { backgroundColor: value }; return ( } @@ -1006,11 +1067,11 @@ InputColor.propTypes = { }; export function FormInputColor({ - hasError, required, className, label, helpMessage, testcid, ...props}) { + hasError, required, className, label, helpMessage, testcid, ...props }) { return ( - + ); } @@ -1023,9 +1084,9 @@ FormInputColor.propTypes = { testcid: PropTypes.string, }; -export function PlainString({controlProps, value}) { +export function PlainString({ controlProps, value }) { let finalValue = value; - if(controlProps?.formatter) { + if (controlProps?.formatter) { finalValue = controlProps.formatter.fromRaw(finalValue); } return {finalValue}; @@ -1035,7 +1096,7 @@ PlainString.propTypes = { value: PropTypes.any, }; -export function FormNote({text, className}) { +export function FormNote({ text, className }) { const classes = useStyles(); return ( @@ -1051,7 +1112,7 @@ FormNote.propTypes = { className: CustomPropTypes.className, }; -const useStylesFormFooter = makeStyles((theme)=>({ +const useStylesFormFooter = makeStyles((theme) => ({ root: { padding: theme.spacing(0.5), position: 'absolute', @@ -1108,7 +1169,7 @@ const useStylesFormFooter = makeStyles((theme)=>({ export function FormFooterMessage(props) { const classes = useStylesFormFooter(); - if(!props.message) { + if (!props.message) { return <>; } return ( @@ -1122,15 +1183,81 @@ FormFooterMessage.propTypes = { message: PropTypes.string, }; -export function NotifierMessage({type=MESSAGE_TYPE.SUCCESS, message, closable=true, onClose=()=>{/*This is intentional (SonarQube)*/}}) { +const useStylesKeyboardShortcut = makeStyles(() => ({ + customRow: { + paddingTop: 5 + } +})); + +export function FormInputKeyboardShortcut({ hasError, label, className, helpMessage, testcid, onChange, ...props }) { + const cid = _.uniqueId('c'); + const helpid = `h${cid}`; + const classes = useStylesKeyboardShortcut(); + return ( + + + + + ); +} +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 ( + + + + + ); +} +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 ( + + + + ); +} + +FormInputThemes.propTypes = { + hasError: PropTypes.bool, + label: PropTypes.string, + className: CustomPropTypes.className, + helpMessage: PropTypes.string, + testcid: PropTypes.string, + onChange: PropTypes.func +}; + + +export function NotifierMessage({ type = MESSAGE_TYPE.SUCCESS, message, closable = true, onClose = () => {/*This is intentional (SonarQube)*/ } }) { const classes = useStylesFormFooter(); return ( - + {message} {closable && - + } ); diff --git a/web/pgadmin/static/js/components/KeyboardShortcuts.jsx b/web/pgadmin/static/js/components/KeyboardShortcuts.jsx new file mode 100644 index 000000000..ec4cf93f8 --- /dev/null +++ b/web/pgadmin/static/js/components/KeyboardShortcuts.jsx @@ -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 ( + + {fields.map(element => { + let ctrlProps = { + label: element.label + }; + if (element.type == 'keyCode') { + return + + {element.label} + + + + + ; + } else if (element.name == 'shift') { + return + + + + ; + } else if (element.name == 'control') { + return + + + + ; + } else if (element.name == 'alt') { + return + + + + ; + } + + })} + + ); +} + +KeyboardShortcuts.propTypes = { + value: PropTypes.object, + onChange: PropTypes.func, + controlProps: PropTypes.object, + fields: PropTypes.array +}; diff --git a/web/pgadmin/static/js/components/QueryThresholds.jsx b/web/pgadmin/static/js/components/QueryThresholds.jsx new file mode 100644 index 000000000..bc0902a80 --- /dev/null +++ b/web/pgadmin/static/js/components/QueryThresholds.jsx @@ -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 ( + + + + {gettext('Warning')} + + + + + + {gettext('Alert')} + + + + + + {gettext('(in minuts)')} + + + + ); +} + +QueryThresholds.propTypes = { + value: PropTypes.object, + onChange: PropTypes.func, +}; \ No newline at end of file diff --git a/web/pgadmin/static/js/components/Themes.jsx b/web/pgadmin/static/js/components/Themes.jsx new file mode 100644 index 000000000..af1034c68 --- /dev/null +++ b/web/pgadmin/static/js/components/Themes.jsx @@ -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 ( + + + + + + {gettext('Preview + + + ); +} + +Themes.propTypes = { + value: PropTypes.string, + onChange: PropTypes.func, + controlProps: PropTypes.object, + fields: PropTypes.array, + options: PropTypes.array, + inputRef: CustomPropTypes.ref +}; \ No newline at end of file diff --git a/web/pgadmin/static/js/helpers/ModalProvider.jsx b/web/pgadmin/static/js/helpers/ModalProvider.jsx index 7e4fe5260..ce6fd8817 100644 --- a/web/pgadmin/static/js/helpers/ModalProvider.jsx +++ b/web/pgadmin/static/js/helpers/ModalProvider.jsx @@ -8,8 +8,8 @@ ////////////////////////////////////////////////////////////// import { Box, Dialog, DialogContent, DialogTitle, makeStyles, Paper } from '@material-ui/core'; -import React from 'react'; -import {getEpoch} from 'sources/utils'; +import React, { useState } from 'react'; +import { getEpoch } from 'sources/utils'; import { DefaultButton, PgIconButton, PrimaryButton } from '../components/Buttons'; import Draggable from 'react-draggable'; import CloseIcon from '@material-ui/icons/CloseRounded'; @@ -19,13 +19,15 @@ import gettext from 'sources/gettext'; import Theme from '../Theme'; import HTMLReactParser from 'html-react-parser'; import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; +import { Rnd } from 'react-rnd'; +import { ExpandDialog, MinimizeDialog } from '../components/ExternalIcon'; const ModalContext = React.createContext({}); export function useModal() { return React.useContext(ModalContext); } -const useAlertStyles = makeStyles((theme)=>({ +const useAlertStyles = makeStyles((theme) => ({ footer: { display: 'flex', justifyContent: 'flex-end', @@ -37,11 +39,11 @@ const useAlertStyles = makeStyles((theme)=>({ } })); -function AlertContent({text, confirm, okLabel=gettext('OK'), cancelLabel=gettext('Cancel'), onOkClick, onCancelClick}) { +function AlertContent({ text, confirm, okLabel = gettext('OK'), cancelLabel = gettext('Cancel'), onOkClick, onCancelClick }) { const classes = useAlertStyles(); return ( - {typeof(text) == 'string' ? HTMLReactParser(text) : text} + {typeof (text) == 'string' ? HTMLReactParser(text) : text} {confirm && } onClick={onCancelClick} >{cancelLabel} @@ -60,10 +62,10 @@ AlertContent.propTypes = { cancelLabel: PropTypes.string, }; -function alert(title, text, onOkClick, okLabel=gettext('OK')){ +function alert(title, text, onOkClick, okLabel = gettext('OK')) { // bind the modal provider before calling - this.showModal(title, (closeModal)=>{ - const onOkClickClose = ()=>{ + this.showModal(title, (closeModal) => { + const onOkClickClose = () => { onOkClick && onOkClick(); closeModal(); }; @@ -73,45 +75,53 @@ function alert(title, text, onOkClick, okLabel=gettext('OK')){ }); } -function confirm(title, text, onOkClick, onCancelClick, okLabel=gettext('Yes'), cancelLabel=gettext('No')) { +function confirm(title, text, onOkClick, onCancelClick, okLabel = gettext('Yes'), cancelLabel = gettext('No')) { // bind the modal provider before calling - this.showModal(title, (closeModal)=>{ - const onCancelClickClose = ()=>{ + this.showModal(title, (closeModal) => { + const onCancelClickClose = () => { onCancelClick && onCancelClick(); closeModal(); }; - const onOkClickClose = ()=>{ + const onOkClickClose = () => { onOkClick && onOkClick(); closeModal(); }; return ( - + ); }); } -export default function ModalProvider({children}) { +export default function ModalProvider({ children }) { const [modals, setModals] = React.useState([]); - const showModal = (title, content, modalOptions)=>{ + const showModal = (title, content, modalOptions) => { let id = getEpoch().toString() + Math.random(); - setModals((prev)=>[...prev, { + setModals((prev) => [...prev, { id: id, title: title, content: content, ...modalOptions, }]); }; - const closeModal = (id)=>{ - setModals((prev)=>{ - return prev.filter((o)=>o.id!=id); + const closeModal = (id) => { + setModals((prev) => { + return prev.filter((o) => o.id != id); }); }; + + const fullScreenModal = (fullScreen) => { + setModals((prev) => [...prev, { + fullScreen: fullScreen, + }]); + }; + const modalContextBase = { showModal: showModal, closeModal: closeModal, + fullScreenModal: fullScreenModal }; - const modalContext = React.useMemo(()=>({ + const modalContext = React.useMemo(() => ({ ...modalContextBase, confirm: confirm.bind(modalContextBase), alert: alert.bind(modalContextBase) @@ -119,8 +129,8 @@ export default function ModalProvider({children}) { return ( {children} - {modals.map((modalOptions, i)=>( - + {modals.map((modalOptions, i) => ( + ))} ); @@ -130,30 +140,118 @@ ModalProvider.propTypes = { children: CustomPropTypes.children, }; +const dialogStyle = makeStyles((theme) => ({ + dialog: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + border: '1px solid ' + theme.otherVars.inputBorderColor, + borderRadius: theme.shape.borderRadius, + }, +})); + function PaperComponent(props) { + let classes = dialogStyle(); + let [dialogPosition, setDialogPosition] = useState(null); + let resizeable = props.isresizeable == 'true' ? true : false; return ( - - - + props.isresizeable == 'true' ? + { + if (props.isfullscreen !== 'true') { + setDialogPosition({ + ...position, + }); + } + }} + onResize={(e, direction, ref, delta, position) => { + setDialogPosition({ + ...position, + }); + }} + > + + + : + + + ); } -function ModalContainer({id, title, content}) { +PaperComponent.propTypes = { + isfullscreen: PropTypes.string, + isresizeable: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, +}; + +const useModalStyles = makeStyles(() => ({ + titleBar: { + display: 'flex', + flexGrow: 1 + }, + title: { + flexGrow: 1 + }, + icon: { + fill: 'currentColor', + width: '1em', + height: '1em', + display: 'inline-block', + fontSize: '1.5rem', + transition: 'none', + flexShrink: 0, + userSelect: 'none', + } +})); + +function ModalContainer({ id, title, content, dialogHeight, dialogWidth, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false }) { let useModalRef = useModal(); - let closeModal = ()=>useModalRef.closeModal(id); + const classes = useModalStyles(); + let closeModal = () => useModalRef.closeModal(id); + const [isfullScreen, setIsFullScreen] = useState(fullScreen); + return ( - {title} - } size="xs" noBorder onClick={closeModal}/> + + {title} + { + showFullScreen && !isfullScreen && + } size="xs" noBorder onClick={() => { setIsFullScreen(!isfullScreen); }} /> + } + { + showFullScreen && isfullScreen && + } size="xs" noBorder onClick={() => { setIsFullScreen(!isfullScreen); }} /> + } + + } size="xs" noBorder onClick={closeModal} /> + - + {content(closeModal)} @@ -164,4 +262,11 @@ ModalContainer.propTypes = { id: PropTypes.string, title: CustomPropTypes.children, content: PropTypes.func, + fullScreen: PropTypes.bool, + maxWidth: PropTypes.string, + isFullWidth: PropTypes.bool, + showFullScreen: PropTypes.bool, + isResizeable: PropTypes.bool, + dialogHeight: PropTypes.number, + dialogWidth: PropTypes.number, }; diff --git a/web/pgadmin/static/js/helpers/Notifier.jsx b/web/pgadmin/static/js/helpers/Notifier.jsx index d67faa381..af11762f8 100644 --- a/web/pgadmin/static/js/helpers/Notifier.jsx +++ b/web/pgadmin/static/js/helpers/Notifier.jsx @@ -8,6 +8,13 @@ ////////////////////////////////////////////////////////////// import { useSnackbar, SnackbarProvider, SnackbarContent } from 'notistack'; +import { makeStyles } from '@material-ui/core/styles'; +import {Box} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/CloseRounded'; +import { DefaultButton, PrimaryButton } from '../components/Buttons'; +import HTMLReactParser from 'html-react-parser'; +import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; +import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; import Theme from 'sources/Theme'; @@ -76,6 +83,41 @@ FinalNotifyContent.propTypes = { children: CustomPropTypes.children, }; +const useModalStyles = makeStyles((theme)=>({ + footer: { + display: 'flex', + justifyContent: 'flex-end', + padding: '0.5rem', + ...theme.mixins.panelBorder.top, + }, + margin: { + marginLeft: '0.25rem', + } +})); +function AlertContent({text, confirm, okLabel=gettext('OK'), cancelLabel=gettext('Cancel'), onOkClick, onCancelClick}) { + const classes = useModalStyles(); + return ( + + {HTMLReactParser(text)} + + {confirm && + } onClick={onCancelClick} >{cancelLabel} + } + } onClick={onOkClick} autoFocus={true} >{okLabel} + + + ); +} +AlertContent.propTypes = { + text: PropTypes.string, + confirm: PropTypes.bool, + onOkClick: PropTypes.func, + onCancelClick: PropTypes.func, + okLabel: PropTypes.string, + cancelLabel: PropTypes.string, +}; + + var Notifier = { success(msg, autoHideDuration = AUTO_HIDE_DURATION) { this._callNotify(msg, MESSAGE_TYPE.SUCCESS, autoHideDuration); @@ -195,11 +237,11 @@ var Notifier = { } modalRef.confirm(title, text, onOkClick, onCancelClick, okLabel, cancelLabel); }, - showModal(title, content) { + showModal: (title, content, modalOptions) => { if(!modalInitialized) { initializeModalProvider(); } - modalRef.showModal(title, content); + modalRef.showModal(title, content, modalOptions); } }; diff --git a/web/pgadmin/static/js/tree/preference_nodes.ts b/web/pgadmin/static/js/tree/preference_nodes.ts new file mode 100644 index 000000000..7fbc8f820 --- /dev/null +++ b/web/pgadmin/static/js/tree/preference_nodes.ts @@ -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((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; +} diff --git a/web/pgadmin/static/js/tree/preferences_tree.tsx b/web/pgadmin/static/js/tree/preferences_tree.tsx new file mode 100644 index 000000000..f19db9de5 --- /dev/null +++ b/web/pgadmin/static/js/tree/preferences_tree.tsx @@ -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( + + , container); +} + +module.exports = { + initPreferencesTree: initPreferencesTree, +}; + diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js index 1087630b4..266bddfc0 100644 --- a/web/pgadmin/static/js/tree/tree.js +++ b/web/pgadmin/static/js/tree/tree.js @@ -14,52 +14,68 @@ import pgAdmin from 'sources/pgadmin'; import { FileType } from 'react-aspen'; import { TreeNode } from './tree_nodes'; -import {isValidData} from 'sources/utils'; +import { isValidData } from 'sources/utils'; function manageTreeEvents(event, eventName, item) { - let d = item ? item._metadata.data: []; + let d = item ? item._metadata.data : []; + let node_metadata = item ? item._metadata : {}; let node; let obj = pgAdmin.Browser; - if (d && obj.Nodes[d._type]) { - node = obj.Nodes[d._type]; - - // If the Browser tree is not initialised yet - if (obj.tree === null) return; - - if (eventName == 'dragstart') { - obj.tree.handleDraggable(event, item); - } - if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') { - obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data') ,item, item.parent.path); - } - if (_.isObject(node.callbacks) && - eventName in node.callbacks && - typeof node.callbacks[eventName] == 'function' && - !node.callbacks[eventName].apply( - node, [item, d, obj, [], eventName])) { - return true; - } - /* Raise tree events for the nodes */ + // Events for preferences tree. + if (node_metadata.parent && node_metadata.parent.includes('/preferences') && obj.ptree.tree.type == 'preferences') { try { - node.trigger( - 'browser-node.' + eventName, node, item, d - ); obj.Events.trigger( - 'pgadmin-browser:tree:' + eventName, item, d, node + 'preferences:tree:' + eventName, item, d ); } catch (e) { console.warn(e.stack || e); return false; } + } else { + // Events for browser tree. + if (d && obj.Nodes[d._type]) { + node = obj.Nodes[d._type]; + + // If the Browser tree is not initialised yet + if (obj.tree === null) return; + + if (eventName == 'dragstart') { + obj.tree.handleDraggable(event, item); + } + if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') { + obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data'), item, item.parent.path); + } + if (_.isObject(node.callbacks) && + eventName in node.callbacks && + typeof node.callbacks[eventName] == 'function' && + !node.callbacks[eventName].apply( + node, [item, d, obj, [], eventName])) { + return true; + } + + /* Raise tree events for the nodes */ + try { + node.trigger( + 'browser-node.' + eventName, node, item, d + ); + obj.Events.trigger( + 'pgadmin-browser:tree:' + eventName, item, d, node + ); + } catch (e) { + console.warn(e.stack || e); + return false; + } + } } return true; } export class Tree { - constructor(tree, manageTree, pgBrowser) { + constructor(tree, manageTree, pgBrowser, type) { this.tree = tree; + this.tree.type = type ? type : 'browser'; this.tree.onTreeEvents(manageTreeEvents); this.rootNode = manageTree.tempTree; @@ -102,12 +118,12 @@ export class Tree { } next(item) { - if(item) { + if (item) { let parent = this.parent(item); - if(parent && parent.children.length > 0) { + if (parent && parent.children.length > 0) { let idx = parent.children.indexOf(item); - if(idx !== -1 && parent.children.length !== idx+1) { - return parent.children[idx+1]; + if (idx !== -1 && parent.children.length !== idx + 1) { + return parent.children[idx + 1]; } } } @@ -115,12 +131,12 @@ export class Tree { } prev(item) { - if(item) { + if (item) { let parent = this.parent(item); - if(parent && parent.children.length > 0) { + if (parent && parent.children.length > 0) { let idx = parent.children.indexOf(item); - if(idx !== -1 && idx !== 0) { - return parent.children[idx-1]; + if (idx !== -1 && idx !== 0) { + return parent.children[idx - 1]; } } } @@ -136,7 +152,7 @@ export class Tree { await item.ensureLoaded(); } - async ensureVisible(item){ + async ensureVisible(item) { await this.tree.ensureVisible(item); } @@ -153,11 +169,11 @@ export class Tree { await this.tree.toggleDirectory(item); } - async select(item, ensureVisible=false, align='auto') { + async select(item, ensureVisible = false, align = 'auto') { await this.tree.setActiveFile(item, ensureVisible, align); } - async selectNode(item, ensureVisible=false, align='auto') { + async selectNode(item, ensureVisible = false, align = 'auto') { this.tree.setActiveFile(item, ensureVisible, align); } @@ -180,18 +196,18 @@ export class Tree { // TBD } async setLabel(item, label) { - if(item) { + if (item) { await this.tree.setLabel(item, label); } } async setInode(item) { - if(item._children) item._children = null; + if (item._children) item._children = null; await this.tree.closeDirectory(item); } async setId(item, data) { - if(item) { + if (item) { item.getMetadata('data').id = data.id; } } @@ -269,7 +285,7 @@ export class Tree { siblings(item) { if (this.hasParent(item)) { let _siblings = this.parent(item).children.filter((_item) => _item.path !== item.path); - if (typeof(_siblings) !== 'object') return [_siblings]; + if (typeof (_siblings) !== 'object') return [_siblings]; else return _siblings; } return []; @@ -294,7 +310,7 @@ export class Tree { } itemData(item) { - return (item !== undefined && item !== null && item.getMetadata('data') !== undefined) ? item._metadata.data : []; + return (item !== undefined && item !== null && item.getMetadata('data') !== undefined) ? item._metadata.data : []; } getData(item) { @@ -323,18 +339,18 @@ export class Tree { findNodeWithToggle(path) { let tree = this; - if(path == null || !Array.isArray(path)) { + if (path == null || !Array.isArray(path)) { return Promise.reject(); } path = '/browser/' + path.join('/'); - let onCorrectPath = function(matchPath) { + let onCorrectPath = function (matchPath) { return (matchPath !== undefined && path !== undefined && (path.startsWith(matchPath) || path === matchPath)); }; return (function findInNode(currentNode) { - return new Promise((resolve, reject)=>{ + return new Promise((resolve, reject) => { if (path === null || path === undefined || path.length === 0) { resolve(null); } @@ -347,18 +363,18 @@ export class Tree { resolve(currentNode); } else { tree.open(currentNode) - .then(()=>{ + .then(() => { let children = currentNode.children; for (let i = 0, length = children.length; i < length; i++) { let childNode = children[i]; - if(onCorrectPath(childNode.path)) { + if (onCorrectPath(childNode.path)) { resolve(findInNode(childNode)); return; } } reject(null); }) - .catch(()=>{ + .catch(() => { reject(null); }); } @@ -368,7 +384,7 @@ export class Tree { findNodeByDomElement(domElement) { const path = domElement.path; - if(!path || !path[0]) { + if (!path || !path[0]) { return undefined; } @@ -390,7 +406,7 @@ export class Tree { createOrUpdateNode(id, data, parent, domNode) { let oldNodePath = id; - if(parent !== null && parent !== undefined && parent.path !== undefined && parent.path != '/browser') { + if (parent !== null && parent !== undefined && parent.path !== undefined && parent.path != '/browser') { oldNodePath = parent.path + '/' + id; } const oldNode = this.findNode(oldNodePath); @@ -456,14 +472,14 @@ export class Tree { * cur is selection range of text after dropping. If returned as * string, by default cursor will be set to the end of text */ - registerDraggableType(typeOrTypeDict, dropDetailsFunc=null) { - if(typeof typeOrTypeDict == 'object') { - Object.keys(typeOrTypeDict).forEach((type)=>{ + registerDraggableType(typeOrTypeDict, dropDetailsFunc = null) { + if (typeof typeOrTypeDict == 'object') { + Object.keys(typeOrTypeDict).forEach((type) => { this.registerDraggableType(type, typeOrTypeDict[type]); }); } else { - if(dropDetailsFunc != null) { - typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type)=>{ + if (dropDetailsFunc != null) { + typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type) => { this.draggableTypes[type] = dropDetailsFunc; }); } @@ -471,7 +487,7 @@ export class Tree { } getDraggable(type) { - if(this.draggableTypes[type]) { + if (this.draggableTypes[type]) { return this.draggableTypes[type]; } else { return null; @@ -482,7 +498,7 @@ export class Tree { let data = item.getMetadata('data'); let dropDetailsFunc = this.getDraggable(data._type); - if(dropDetailsFunc != null) { + if (dropDetailsFunc != null) { /* addEventListener is used here because import jquery.drag.event * overrides the dragstart event set using element.on('dragstart') @@ -490,20 +506,20 @@ export class Tree { */ let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item)); - if(typeof dropDetails == 'string') { + if (typeof dropDetails == 'string') { dropDetails = { - text:dropDetails, - cur:{ - from:dropDetails.length, + text: dropDetails, + cur: { + from: dropDetails.length, to: dropDetails.length, }, }; } else { - if(!dropDetails.cur) { + if (!dropDetails.cur) { dropDetails = { ...dropDetails, - cur:{ - from:dropDetails.text.length, + cur: { + from: dropDetails.text.length, to: dropDetails.text.length, }, }; @@ -512,14 +528,14 @@ export class Tree { e.dataTransfer.setData('text', JSON.stringify(dropDetails)); /* Required by Firefox */ - if(e.dataTransfer.dropEffect) { + if (e.dataTransfer.dropEffect) { e.dataTransfer.dropEffect = 'move'; } /* setDragImage is not supported in IE. We leave it to * its default look and feel */ - if(e.dataTransfer.setDragImage) { + if (e.dataTransfer.setDragImage) { let dragItem = $(`
${_.escape(dropDetails.text)} @@ -576,4 +592,4 @@ export function findInTree(rootNode, path) { let isValidTreeNodeData = isValidData; -export {isValidTreeNodeData}; +export { isValidTreeNodeData }; diff --git a/web/pgadmin/static/scss/_alertify.overrides.scss b/web/pgadmin/static/scss/_alertify.overrides.scss index ddaaee18d..f70207be9 100644 --- a/web/pgadmin/static/scss/_alertify.overrides.scss +++ b/web/pgadmin/static/scss/_alertify.overrides.scss @@ -1,5 +1,8 @@ /* Overrides alertify js headers */ .alertify { + z-index: 3000; + position: fixed; + .ajs-header { padding: 6px 10px !important; //margin is calculated with -$alertify-borderremove-margin, adjust the header diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index 46a326da1..b18145e0f 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -197,7 +197,7 @@ def register_query_tool_preferences(self): options=[{'label': gettext('None'), 'value': 'none'}, {'label': gettext('All'), 'value': 'all'}, {'label': gettext('Strings'), 'value': 'strings'}], - select2={ + control_props={ 'allowClear': False, 'tags': False } @@ -209,9 +209,9 @@ def register_query_tool_preferences(self): category_label=PREF_LABEL_CSV_TXT, options=[{'label': '"', 'value': '"'}, {'label': '\'', 'value': '\''}], - select2={ + control_props={ 'allowClear': False, - 'tags': True + 'tags': False } ) @@ -223,9 +223,9 @@ def register_query_tool_preferences(self): {'label': ',', 'value': ','}, {'label': '|', 'value': '|'}, {'label': gettext('Tab'), 'value': '\t'}], - select2={ + control_props={ 'allowClear': False, - 'tags': True + 'tags': False } ) @@ -247,7 +247,7 @@ def register_query_tool_preferences(self): options=[{'label': gettext('None'), 'value': 'none'}, {'label': gettext('All'), 'value': 'all'}, {'label': gettext('Strings'), 'value': 'strings'}], - select2={ + control_props={ 'allowClear': False, 'tags': False } @@ -259,9 +259,9 @@ def register_query_tool_preferences(self): category_label=PREF_LABEL_RESULTS_GRID, options=[{'label': '"', 'value': '"'}, {'label': '\'', 'value': '\''}], - select2={ + control_props={ 'allowClear': False, - 'tags': True + 'tags': False } ) @@ -273,9 +273,9 @@ def register_query_tool_preferences(self): {'label': ',', 'value': ','}, {'label': '|', 'value': '|'}, {'label': gettext('Tab'), 'value': '\t'}], - select2={ + control_props={ 'allowClear': False, - 'tags': True + 'tags': False } ) diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py index 090118041..e07424cb7 100644 --- a/web/pgadmin/utils/preferences.py +++ b/web/pgadmin/utils/preferences.py @@ -66,10 +66,11 @@ class _Preference(object): self.label = label self._type = _type self.help_str = kwargs.get('help_str', None) + self.control_props = kwargs.get('control_props', None) self.min_val = kwargs.get('min_val', None) self.max_val = kwargs.get('max_val', None) self.options = kwargs.get('options', None) - self.select2 = kwargs.get('select2', None) + self.select = kwargs.get('select', None) self.fields = kwargs.get('fields', None) self.allow_blanks = kwargs.get('allow_blanks', None) self.disabled = kwargs.get('disabled', False) @@ -146,10 +147,10 @@ class _Preference(object): for opt in self.options: if 'value' in opt and opt['value'] == res.value: return True, res.value - if self.select2 and self.select2['tags']: + if self.select and self.select['tags']: return True, res.value return True, self.default - if self._type == 'select2': + if self._type == 'select': if res.value: res.value = res.value.replace('[', '') res.value = res.value.replace(']', '') @@ -190,7 +191,7 @@ class _Preference(object): has_value = next((True for opt in self.options if 'value' in opt and opt['value'] == value), False) - assert (has_value or (self.select2 and self.select2['tags'])) + assert (has_value or (self.select and self.select['tags'])) elif self._type == 'date': value = parser_map[self._type](value).date() else: @@ -248,10 +249,11 @@ class _Preference(object): 'label': self.label or self.name, 'type': self._type, 'help_str': self.help_str, + 'control_props': self.control_props, 'min_val': self.min_val, 'max_val': self.max_val, 'options': self.options, - 'select2': self.select2, + 'select': self.select, 'value': self.get(), 'fields': self.fields, 'disabled': self.disabled, @@ -393,7 +395,7 @@ class Preferences(object): return res def register( - self, category, name, label, _type, default, **kwargs + self, category, name, label, _type, default, **kwargs ): """ register @@ -414,7 +416,7 @@ class Preferences(object): :param options: :param help_str: :param category_label: - :param select2: select2 control extra options + :param select: select control extra options :param fields: field schema (if preference has more than one field to take input from user e.g. keyboardshortcut preference) :param allow_blanks: Flag specify whether to allow blank value. @@ -424,8 +426,9 @@ class Preferences(object): max_val = kwargs.get('max_val', None) options = kwargs.get('options', None) help_str = kwargs.get('help_str', None) + control_props = kwargs.get('control_props', {}) category_label = kwargs.get('category_label', None) - select2 = kwargs.get('select2', None) + select = kwargs.get('select', None) fields = kwargs.get('fields', None) allow_blanks = kwargs.get('allow_blanks', None) disabled = kwargs.get('disabled', False) @@ -440,14 +443,15 @@ class Preferences(object): assert _type in ( 'boolean', 'integer', 'numeric', 'date', 'datetime', 'options', 'multiline', 'switch', 'node', 'text', 'radioModern', - 'keyboardshortcut', 'select2', 'selectFile', 'threshold' + 'keyboardshortcut', 'select', 'selectFile', 'threshold' ), "Type cannot be found in the defined list!" (cat['preferences'])[name] = res = _Preference( cat['id'], name, label, _type, default, help_str=help_str, min_val=min_val, max_val=max_val, options=options, - select2=select2, fields=fields, allow_blanks=allow_blanks, - disabled=disabled, dependents=dependents + select=select, fields=fields, allow_blanks=allow_blanks, + disabled=disabled, dependents=dependents, + control_props=control_props ) return res @@ -483,7 +487,7 @@ class Preferences(object): @classmethod def register_preference( - cls, module, category, name, label, _type, **kwargs + cls, module, category, name, label, _type, **kwargs ): """ register @@ -503,6 +507,7 @@ class Preferences(object): max_val = kwargs.get('max_val', None) options = kwargs.get('options', None) help_str = kwargs.get('help_str', None) + control_props = kwargs.get('control_props', None) module_label = kwargs.get('module_label', None) category_label = kwargs.get('category_label', None) @@ -516,6 +521,7 @@ class Preferences(object): return m.register( category, name, label, _type, default, min_val=min_val, max_val=max_val, options=options, help_str=help_str, + control_props=control_props, category_label=category_label ) diff --git a/web/regression/javascript/components/KeyboardShortcuts.spec.js b/web/regression/javascript/components/KeyboardShortcuts.spec.js new file mode 100644 index 000000000..67695308a --- /dev/null +++ b/web/regression/javascript/components/KeyboardShortcuts.spec.js @@ -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( + ); + }); + + 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'); + }); + + }); +}); diff --git a/web/regression/javascript/components/QueryThreshold.spec.js b/web/regression/javascript/components/QueryThreshold.spec.js new file mode 100644 index 000000000..fa259ae0d --- /dev/null +++ b/web/regression/javascript/components/QueryThreshold.spec.js @@ -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( + ); + }); + + 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); + }); + }); + +}); diff --git a/web/regression/javascript/components/Themes.spec.js b/web/regression/javascript/components/Themes.spec.js new file mode 100644 index 000000000..545716528 --- /dev/null +++ b/web/regression/javascript/components/Themes.spec.js @@ -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( + ); + }); + + 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'); + }); + }); + +}); diff --git a/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js b/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js new file mode 100644 index 000000000..9c37f52d2 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/binary_path.ui.spec.js @@ -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); + }); + +}); diff --git a/web/webpack.shim.js b/web/webpack.shim.js index f45efde88..a2e8f28e7 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -282,7 +282,7 @@ var webpackShimConfig = { 'pgadmin.node.user_mapping': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/user_mappings/static/js/user_mapping'), 'pgadmin.node.view': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view'), 'pgadmin.node.row_security_policy': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/row_security_policies/static/js/row_security_policy'), - 'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/preferences'), + 'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/'), 'pgadmin.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'), 'pgadmin.server.supported_servers': '/browser/server/supported_servers', 'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'), diff --git a/web/yarn.lock b/web/yarn.lock index 7dc709ab5..c4a222b09 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -91,19 +91,19 @@ json5 "^2.1.2" semver "^6.3.0" -"@babel/eslint-parser@^7.12.13": - version "7.13.8" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.13.8.tgz#6f2bde6b0690fcc0598b4869fc7c8e8b55b17687" - integrity sha512-XewKkiyukrGzMeqToXJQk6hjg2veI9SNQElGzAoAjKxYCLbgcVX4KA2WhoyqMon9N4RMdCZhNTJNOBcp9spsiw== +"@babel/eslint-parser@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz#eabb24ad9f0afa80e5849f8240d0e5facc2d90d6" + integrity sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA== dependencies: - eslint-scope "5.1.0" - eslint-visitor-keys "^1.3.0" + eslint-scope "^5.1.1" + eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/eslint-plugin@^7.12.13": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.13.0.tgz#e6d99efcd6b8551adf479e382a47218726179b1b" - integrity sha512-YGwCLc/u/uc3bU+q/fvgRQ62+TkxuyVvdmybK6ElzE49vODp+RnRe16eJzMM7EwvcRPQfQvcOSuGmzfcbZE2+w== +"@babel/eslint-plugin@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.17.7.tgz#4ee1d5b29b79130f3bb5a933358376bcbee172b8" + integrity sha512-JATUoJJXSgwI0T8juxWYtK1JSgoLpIGUsCHIv+NMXcUDA2vIe6nvAHR9vnuJgs/P1hOFw7vPwibixzfqBBLIVw== dependencies: eslint-rule-composer "^0.3.0" @@ -5223,14 +5223,6 @@ eslint-rule-composer@^0.3.0: resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== -eslint-scope@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -5251,7 +5243,7 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint-visitor-keys@^2.0.0: +eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== @@ -5320,7 +5312,7 @@ esquery@^1.4.0: dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0, esrecurse@^4.3.0: +esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== @@ -5498,6 +5490,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-memoize@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e" + integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw== + fast-safe-stringify@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" @@ -8351,9 +8348,9 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -"pgadmin4-tree@git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422": +"pgadmin4-tree@git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613": version "1.0.0" - resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422" + resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#c966febebcdffaa46f1ccf0769fe5308f179d613" dependencies: "@types/classnames" "^2.2.6" "@types/react" "^16.7.18" @@ -9023,6 +9020,13 @@ rc-util@^5.12.0, rc-util@^5.15.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0 react-is "^16.12.0" shallowequal "^1.1.0" +re-resizable@6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.1.tgz#6be082b55d02364ca4bfee139e04feebdf52441c" + integrity sha512-KRYAgr9/j1PJ3K+t+MBhlQ+qkkoLDJ1rs0z1heIWvYbCW/9Vq4djDU+QumJ3hQbwwtzXF6OInla6rOx6hhgRhQ== + dependencies: + fast-memoize "^2.5.1" + react-aspen@^1.1.0, react-aspen@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/react-aspen/-/react-aspen-1.1.1.tgz#61a85ef43748158322c4a3b73faaa5e563edd038" @@ -9063,6 +9067,14 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-draggable@4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3" + integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w== + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + react-draggable@^4.4.4: version "4.4.4" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f" @@ -9093,6 +9105,15 @@ react-property@1.0.1: resolved "https://registry.yarnpkg.com/react-property/-/react-property-1.0.1.tgz#4ae4211557d0a0ae050a71aa8ad288c074bea4e6" integrity sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ== +react-rnd@^10.3.5: + version "10.3.5" + resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.3.5.tgz#b66e5e06f1eb6823e72eb4b552081b4b9241b139" + integrity sha512-LWJP+l5bp76sDPKrKM8pwGJifI6i3B5jHK4ONACczVMbR8ycNGA75ORRqpRuXGyKawUs68s1od05q8cqWgQXgw== + dependencies: + re-resizable "6.9.1" + react-draggable "4.4.3" + tslib "2.3.0" + react-select@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" @@ -10511,6 +10532,11 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +tslib@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tslib@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"