mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Port preferences dialog to React. Fixes #7149
This commit is contained in:
committed by
Akshay Joshi
parent
3299b0c1b0
commit
74e794b416
@@ -37,26 +37,23 @@ class PreferencesModule(PgAdminModule):
|
||||
"""
|
||||
|
||||
def get_own_javascripts(self):
|
||||
return [{
|
||||
'name': 'pgadmin.preferences',
|
||||
'path': url_for('preferences.index') + 'preferences',
|
||||
'when': None
|
||||
}]
|
||||
scripts = list()
|
||||
for name, script in [
|
||||
['pgadmin.preferences', 'js/preferences']
|
||||
]:
|
||||
scripts.append({
|
||||
'name': name,
|
||||
'path': url_for('preferences.index') + script,
|
||||
'when': None
|
||||
})
|
||||
|
||||
return scripts
|
||||
|
||||
def get_own_stylesheets(self):
|
||||
return []
|
||||
|
||||
def get_own_menuitems(self):
|
||||
return {
|
||||
'file_items': [
|
||||
MenuItem(name='mnu_preferences',
|
||||
priority=997,
|
||||
module="pgAdmin.Preferences",
|
||||
callback='show',
|
||||
icon='fa fa-cog',
|
||||
label=gettext('Preferences'))
|
||||
]
|
||||
}
|
||||
return {}
|
||||
|
||||
def get_exposed_url_endpoints(self):
|
||||
"""
|
||||
@@ -149,7 +146,8 @@ def _iterate_categories(pref_d, label, res):
|
||||
"label": gettext(pref_d['label']),
|
||||
"inode": True,
|
||||
"open": True,
|
||||
"branch": []
|
||||
"children": [],
|
||||
"value": gettext(pref_d['label']),
|
||||
}
|
||||
|
||||
for c in pref_d['categories']:
|
||||
@@ -162,13 +160,15 @@ def _iterate_categories(pref_d, label, res):
|
||||
"id": c['id'],
|
||||
"mid": pref_d['id'],
|
||||
"label": gettext(c['label']),
|
||||
"value": '{0}{1}'.format(c['id'], gettext(c['label'])),
|
||||
"inode": False,
|
||||
"open": False,
|
||||
"preferences": sorted(c['preferences'], key=label)
|
||||
"preferences": sorted(c['preferences'], key=label),
|
||||
"showCheckbox": False
|
||||
}
|
||||
|
||||
(om['branch']).append(oc)
|
||||
om['branch'] = sorted(om['branch'], key=label)
|
||||
(om['children']).append(oc)
|
||||
om['children'] = sorted(om['children'], key=label)
|
||||
|
||||
res.append(om)
|
||||
|
||||
@@ -194,53 +194,69 @@ def preferences_s():
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route("/<int:pid>", methods=["PUT"], endpoint="update")
|
||||
def get_data():
|
||||
"""
|
||||
Get preferences data.
|
||||
:return: Preferences list
|
||||
:rtype: list
|
||||
"""
|
||||
pref_data = request.form if request.form else json.loads(
|
||||
request.data.decode())
|
||||
|
||||
if not pref_data:
|
||||
raise ValueError("Please provide the valid preferences data to save.")
|
||||
|
||||
return pref_data
|
||||
|
||||
|
||||
@blueprint.route("/", methods=["PUT"], endpoint="update")
|
||||
@login_required
|
||||
def save(pid):
|
||||
def save():
|
||||
"""
|
||||
Save a specific preference.
|
||||
"""
|
||||
data = request.form if request.form else json.loads(request.data.decode())
|
||||
pref_data = get_data()
|
||||
|
||||
if data['name'] in ['vw_edt_tab_title_placeholder',
|
||||
'qt_tab_title_placeholder',
|
||||
'debugger_tab_title_placeholder'] \
|
||||
and data['value'].isspace():
|
||||
data['value'] = ''
|
||||
for data in pref_data:
|
||||
if data['name'] in ['vw_edt_tab_title_placeholder',
|
||||
'qt_tab_title_placeholder',
|
||||
'debugger_tab_title_placeholder'] \
|
||||
and data['value'].isspace():
|
||||
data['value'] = ''
|
||||
|
||||
res, msg = Preferences.save(
|
||||
data['mid'], data['category_id'], data['id'], data['value'])
|
||||
sgm.get_nodes(sgm)
|
||||
res, msg = Preferences.save(
|
||||
data['mid'], data['category_id'], data['id'], data['value'])
|
||||
sgm.get_nodes(sgm)
|
||||
|
||||
if not res:
|
||||
return internal_server_error(errormsg=msg)
|
||||
if not res:
|
||||
return internal_server_error(errormsg=msg)
|
||||
|
||||
response = success_return()
|
||||
response = success_return()
|
||||
|
||||
# Set cookie & session for language settings.
|
||||
# This will execute every time as could not find the better way to know
|
||||
# that which preference is getting updated.
|
||||
# Set cookie & session for language settings.
|
||||
# This will execute every time as could not find the better way to know
|
||||
# that which preference is getting updated.
|
||||
|
||||
misc_preference = Preferences.module('misc')
|
||||
user_languages = misc_preference.preference(
|
||||
'user_language'
|
||||
)
|
||||
misc_preference = Preferences.module('misc')
|
||||
user_languages = misc_preference.preference(
|
||||
'user_language'
|
||||
)
|
||||
|
||||
language = 'en'
|
||||
if user_languages:
|
||||
language = user_languages.get() or language
|
||||
language = 'en'
|
||||
if user_languages:
|
||||
language = user_languages.get() or language
|
||||
|
||||
domain = dict()
|
||||
if config.COOKIE_DEFAULT_DOMAIN and\
|
||||
config.COOKIE_DEFAULT_DOMAIN != 'localhost':
|
||||
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
|
||||
domain = dict()
|
||||
if config.COOKIE_DEFAULT_DOMAIN and \
|
||||
config.COOKIE_DEFAULT_DOMAIN != 'localhost':
|
||||
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
|
||||
|
||||
setattr(session, 'PGADMIN_LANGUAGE', language)
|
||||
response.set_cookie("PGADMIN_LANGUAGE", value=language,
|
||||
path=config.COOKIE_DEFAULT_PATH,
|
||||
secure=config.SESSION_COOKIE_SECURE,
|
||||
httponly=config.SESSION_COOKIE_HTTPONLY,
|
||||
samesite=config.SESSION_COOKIE_SAMESITE,
|
||||
**domain)
|
||||
setattr(session, 'PGADMIN_LANGUAGE', language)
|
||||
response.set_cookie("PGADMIN_LANGUAGE", value=language,
|
||||
path=config.COOKIE_DEFAULT_PATH,
|
||||
secure=config.SESSION_COOKIE_SECURE,
|
||||
httponly=config.SESSION_COOKIE_HTTPONLY,
|
||||
samesite=config.SESSION_COOKIE_SAMESITE,
|
||||
**domain)
|
||||
|
||||
return response
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FileType } from 'react-aspen';
|
||||
import { Box } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import SchemaView from '../../../../static/js/SchemaView';
|
||||
import getApiInstance from '../../../../static/js/api_instance';
|
||||
import CloseSharpIcon from '@material-ui/icons/CloseSharp';
|
||||
import HelpIcon from '@material-ui/icons/HelpRounded';
|
||||
import SaveSharpIcon from '@material-ui/icons/SaveSharp';
|
||||
import clsx from 'clsx';
|
||||
import Notify from '../../../../static/js/helpers/Notifier';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import { DefaultButton, PgIconButton, PrimaryButton } from '../../../../static/js/components/Buttons';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import { getBinaryPathSchema } from '../../../../browser/server_groups/servers/static/js/binary_path.ui';
|
||||
import { _set_dynamic_tab } from '../../../../tools/datagrid/static/js/show_query_tool';
|
||||
|
||||
class PreferencesSchema extends BaseUISchema {
|
||||
constructor(initValues = {}, schemaFields = []) {
|
||||
super({
|
||||
...initValues
|
||||
});
|
||||
this.schemaFields = schemaFields;
|
||||
this.category = '';
|
||||
}
|
||||
|
||||
get idAttribute() {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
setSelectedCategory(category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return this.schemaFields;
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
overflow: 'hidden',
|
||||
'&$disabled': {
|
||||
color: '#ddd',
|
||||
}
|
||||
},
|
||||
body: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
preferences: {
|
||||
borderColor: theme.otherVars.borderColor,
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
minHeight: 0,
|
||||
overflow: 'hidden'
|
||||
|
||||
},
|
||||
treeContainer: {
|
||||
flexBasis: '25%',
|
||||
alignItems: 'flex-start',
|
||||
paddingLeft: '5px',
|
||||
minHeight: 0,
|
||||
flexGrow: 1
|
||||
},
|
||||
tree: {
|
||||
height: '100%',
|
||||
flexGrow: 1
|
||||
},
|
||||
preferencesContainer: {
|
||||
flexBasis: '75%',
|
||||
padding: '5px',
|
||||
borderColor: theme.otherVars.borderColor + '!important',
|
||||
borderLeft: '1px solid',
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
paddingTop: '5px',
|
||||
overflow: 'auto'
|
||||
},
|
||||
actionBtn: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
buttonMargin: {
|
||||
marginLeft: '0.5em'
|
||||
},
|
||||
footer: {
|
||||
borderTop: '1px solid #dde0e6 !important',
|
||||
padding: '0.5rem',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: theme.otherVars.headerBg,
|
||||
},
|
||||
customTreeClass: {
|
||||
'& .react-checkbox-tree': {
|
||||
height: '100% !important',
|
||||
border: 'none !important',
|
||||
},
|
||||
},
|
||||
preferencesTree: {
|
||||
height: 'calc(100% - 50px)',
|
||||
minHeight: 0
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
function RightPanel({ schema, ...props }) {
|
||||
let initData = () => new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(props.initValues);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={initData}
|
||||
viewHelperProps={{ mode: 'edit' }}
|
||||
schema={schema}
|
||||
showFooter={false}
|
||||
isTabView={false}
|
||||
onDataChange={(isChanged, changedData) => {
|
||||
props.onDataChange(changedData);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RightPanel.propTypes = {
|
||||
schema: PropTypes.object,
|
||||
initValues: PropTypes.object,
|
||||
onDataChange: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
export default function PreferencesComponent({ ...props }) {
|
||||
const classes = useStyles();
|
||||
const [disableSave, setDisableSave] = React.useState(true);
|
||||
const prefSchema = React.useRef(new PreferencesSchema({}, []));
|
||||
const prefChangedData = React.useRef({});
|
||||
const prefTreeInit = React.useRef(false);
|
||||
const [prefTreeData, setPrefTreeData] = React.useState(null);
|
||||
const [initValues, setInitValues] = React.useState({});
|
||||
const [loadTree, setLoadTree] = React.useState(0);
|
||||
const api = getApiInstance();
|
||||
|
||||
useEffect(() => {
|
||||
const pref_url = url_for('preferences.index');
|
||||
api({
|
||||
url: pref_url,
|
||||
method: 'GET',
|
||||
}).then((res) => {
|
||||
let preferencesData = [];
|
||||
let preferencesTreeData = [];
|
||||
let preferencesValues = {};
|
||||
res.data.forEach(node => {
|
||||
let id = Math.floor(Math.random() * 1000);
|
||||
let tdata = {
|
||||
'id': id.toString(),
|
||||
'label': node.label,
|
||||
'_label': node.label,
|
||||
'name': node.label,
|
||||
'icon': '',
|
||||
'inode': true,
|
||||
'type': 2,
|
||||
'_type': node.label.toLowerCase(),
|
||||
'_id': id,
|
||||
'_pid': null,
|
||||
'childrenNodes': [],
|
||||
'expanded': true,
|
||||
'isExpanded': true,
|
||||
};
|
||||
|
||||
node.children.forEach(subNode => {
|
||||
let sid = Math.floor(Math.random() * 1000);
|
||||
let nodeData = {
|
||||
'id': sid.toString(),
|
||||
'label': subNode.label,
|
||||
'_label': subNode.label,
|
||||
'name': subNode.label,
|
||||
'icon': '',
|
||||
'inode': false,
|
||||
'_type': subNode.label.toLowerCase(),
|
||||
'_id': sid,
|
||||
'_pid': node.id,
|
||||
'type': 1,
|
||||
'expanded': false,
|
||||
};
|
||||
|
||||
if (subNode.label == 'Nodes' && node.label == 'Browser') {
|
||||
//Add Note for Nodes
|
||||
preferencesData.push(
|
||||
{
|
||||
id: 'note_' + subNode.id,
|
||||
type: 'note', text: [gettext('This settings is to Show/Hide nodes in the browser tree.')].join(''),
|
||||
visible: false,
|
||||
'parentId': nodeData['id']
|
||||
},
|
||||
);
|
||||
}
|
||||
subNode.preferences.forEach((element) => {
|
||||
let addNote = false;
|
||||
let note = '';
|
||||
let type = getControlMappedForType(element.type);
|
||||
|
||||
if (type === 'file') {
|
||||
addNote = true;
|
||||
note = gettext('Enter the directory in which the psql, pg_dump, pg_dumpall, and pg_restore utilities can be found for the corresponding database server version. The default path will be used for server versions that do not have a path specified.');
|
||||
element.type = 'collection';
|
||||
element.schema = getBinaryPathSchema();
|
||||
element.canAdd = false;
|
||||
element.canDelete = false;
|
||||
element.canEdit = false;
|
||||
element.editable = false;
|
||||
element.disabled = true;
|
||||
preferencesValues[element.id] = JSON.parse(element.value);
|
||||
}
|
||||
else if (type == 'select') {
|
||||
if (element.control_props !== undefined) {
|
||||
element.controlProps = element.control_props;
|
||||
} else {
|
||||
element.controlProps = {};
|
||||
}
|
||||
|
||||
element.type = type;
|
||||
preferencesValues[element.id] = element.value;
|
||||
|
||||
if (element.name == 'theme') {
|
||||
element.type = 'theme';
|
||||
|
||||
element.options.forEach((opt) => {
|
||||
if (opt.value == element.value) {
|
||||
opt.selected = true;
|
||||
} else {
|
||||
opt.selected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (type === 'keyboardShortcut') {
|
||||
element.type = 'keyboardShortcut';
|
||||
element.canAdd = false;
|
||||
element.canDelete = false;
|
||||
element.canEdit = false;
|
||||
element.editable = false;
|
||||
if (pgAdmin.Browser.get_preference(node.label.toLowerCase(), element.name)?.value) {
|
||||
let temp = pgAdmin.Browser.get_preference(node.label.toLowerCase(), element.name).value;
|
||||
preferencesValues[element.id] = temp;
|
||||
} else {
|
||||
preferencesValues[element.id] = element.value;
|
||||
}
|
||||
delete element.value;
|
||||
} else if (type === 'threshold') {
|
||||
element.type = 'threshold';
|
||||
|
||||
let _val = element.value.split('|');
|
||||
preferencesValues[element.id] = { 'warning': _val[0], 'alert': _val[1] };
|
||||
} else {
|
||||
element.type = type;
|
||||
preferencesValues[element.id] = element.value;
|
||||
}
|
||||
|
||||
delete element.value;
|
||||
element.visible = false;
|
||||
element.helpMessage = element?.help_str ? element.help_str : null;
|
||||
preferencesData.push(element);
|
||||
|
||||
if (addNote) {
|
||||
preferencesData.push(
|
||||
{
|
||||
id: 'note_' + element.id,
|
||||
type: 'note', text: [
|
||||
'<ul><li>',
|
||||
gettext(note),
|
||||
'</li></ul>',
|
||||
].join(''),
|
||||
visible: false,
|
||||
'parentId': nodeData['id']
|
||||
},
|
||||
);
|
||||
}
|
||||
element.parentId = nodeData['id'];
|
||||
});
|
||||
tdata['childrenNodes'].push(nodeData);
|
||||
});
|
||||
|
||||
// set Preferences Tree data
|
||||
preferencesTreeData.push(tdata);
|
||||
|
||||
});
|
||||
setPrefTreeData(preferencesTreeData);
|
||||
setInitValues(preferencesValues);
|
||||
// set Preferences schema
|
||||
prefSchema.current = new PreferencesSchema(preferencesValues, preferencesData);
|
||||
}).catch((err) => {
|
||||
Notify.alert(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
props.renderTree(prefTreeData);
|
||||
let initTreeTimeout = null;
|
||||
|
||||
// Listen selected preferences tree node event and show the appropriate components in right panel.
|
||||
pgAdmin.Browser.Events.on('preferences:tree:selected', (item) => {
|
||||
if (item.type == FileType.File) {
|
||||
prefSchema.current.schemaFields.forEach((field) => {
|
||||
field.visible = field.parentId === item._metadata.data.id;
|
||||
});
|
||||
setLoadTree(Math.floor(Math.random() * 1000));
|
||||
initTreeTimeout = setTimeout(()=> {
|
||||
prefTreeInit.current = true;
|
||||
}, 10);
|
||||
}
|
||||
else {
|
||||
if(item.isExpanded && item._children && item._children.length > 0 && prefTreeInit.current) {
|
||||
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen open preferences tree node event to default select first child node on parent node selection.
|
||||
pgAdmin.Browser.Events.on('preferences:tree:opened', (item) => {
|
||||
if (item._fileName == 'Browser' && item.type == 2 && item.isExpanded && item._children && item._children.length > 0 && !prefTreeInit.current) {
|
||||
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], false);
|
||||
}
|
||||
else if(prefTreeInit.current) {
|
||||
pgAdmin.Browser.ptree.tree.setActiveFile(item._children[0], true);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen added preferences tree node event to expand the newly added node on tree load.
|
||||
pgAdmin.Browser.Events.on('preferences:tree:added', (item) => {
|
||||
// Check the if newely added node is Directoy call toggle to expand the node.
|
||||
if (item.type == FileType.Directory) {
|
||||
pgAdmin.Browser.ptree.tree.toggleDirectory(item);
|
||||
}
|
||||
});
|
||||
|
||||
/* Clear the initTreeTimeout timeout if unmounted */
|
||||
return ()=>{
|
||||
clearTimeout(initTreeTimeout);
|
||||
};
|
||||
}, [prefTreeData]);
|
||||
|
||||
|
||||
|
||||
function getControlMappedForType(type) {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return 'text';
|
||||
case 'input':
|
||||
return 'text';
|
||||
case 'boolean':
|
||||
return 'switch';
|
||||
case 'node':
|
||||
return 'switch';
|
||||
case 'integer':
|
||||
return 'numeric';
|
||||
case 'numeric':
|
||||
return 'numeric';
|
||||
case 'date':
|
||||
return 'datetimepicker';
|
||||
case 'datetime':
|
||||
return 'datetimepicker';
|
||||
case 'options':
|
||||
return 'select';
|
||||
case 'select':
|
||||
return 'select';
|
||||
case 'select2':
|
||||
return 'select';
|
||||
case 'multiline':
|
||||
return 'multiline';
|
||||
case 'switch':
|
||||
return 'switch';
|
||||
case 'keyboardshortcut':
|
||||
return 'keyboardShortcut';
|
||||
case 'radioModern':
|
||||
return 'toggle';
|
||||
case 'selectFile':
|
||||
return 'file';
|
||||
case 'threshold':
|
||||
return 'threshold';
|
||||
default:
|
||||
if (console && console.warn) {
|
||||
// Warning for developer only.
|
||||
console.warn(
|
||||
'Hmm.. We don\'t know how to render this type - \'\'' + type + '\' of control.'
|
||||
);
|
||||
}
|
||||
return 'input';
|
||||
}
|
||||
}
|
||||
|
||||
function getCollectionValue(_metadata, value, initValues) {
|
||||
let val = value;
|
||||
if (typeof (value) == 'object') {
|
||||
if (_metadata[0].type == 'collection' && _metadata[0].schema) {
|
||||
if ('binaryPath' in value.changed[0]) {
|
||||
let pathData = [];
|
||||
let pathVersions = [];
|
||||
value.changed.forEach((chValue) => {
|
||||
pathVersions.push(chValue.version);
|
||||
});
|
||||
initValues[_metadata[0].id].forEach((initVal) => {
|
||||
if (pathVersions.includes(initVal.version)) {
|
||||
pathData.push(value.changed[pathVersions.indexOf(initVal.version)]);
|
||||
}
|
||||
else {
|
||||
pathData.push(initVal);
|
||||
}
|
||||
});
|
||||
val = JSON.stringify(pathData);
|
||||
} else {
|
||||
let key_val = {
|
||||
'char': value.changed[0]['key'],
|
||||
'key_code': value.changed[0]['code'],
|
||||
};
|
||||
value.changed[0]['key'] = key_val;
|
||||
val = value.changed[0];
|
||||
}
|
||||
} else if ('warning' in value) {
|
||||
val = value['warning'] + '|' + value['alert'];
|
||||
} else if (value?.changed && value.changed.length > 0) {
|
||||
val = JSON.stringify(value.changed);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function savePreferences(data, initValues) {
|
||||
let _data = [];
|
||||
for (const [key, value] of Object.entries(data.current)) {
|
||||
let _metadata = prefSchema.current.schemaFields.filter((el) => { return el.id == key; });
|
||||
if (_metadata.length > 0) {
|
||||
let val = getCollectionValue(_metadata, value, initValues);
|
||||
_data.push({
|
||||
'category_id': _metadata[0]['cid'],
|
||||
'id': parseInt(key),
|
||||
'mid': _metadata[0]['mid'],
|
||||
'name': _metadata[0]['name'],
|
||||
'value': val,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_data.length > 0) {
|
||||
save(_data, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function checkRefreshRequired(pref, requires_refresh) {
|
||||
if (pref.name == 'theme') {
|
||||
requires_refresh = true;
|
||||
}
|
||||
|
||||
if (pref.name == 'user_language') {
|
||||
requires_refresh = true;
|
||||
}
|
||||
|
||||
return requires_refresh;
|
||||
}
|
||||
|
||||
function save(save_data, data) {
|
||||
api({
|
||||
url: url_for('preferences.index'),
|
||||
method: 'PUT',
|
||||
data: save_data,
|
||||
}).then(() => {
|
||||
let requires_refresh = false;
|
||||
/* Find the modules changed */
|
||||
let modulesChanged = {};
|
||||
for (const [key] of Object.entries(data.current)) {
|
||||
let pref = pgAdmin.Browser.get_preference_for_id(Number(key));
|
||||
|
||||
if (pref['name'] == 'dynamic_tabs') {
|
||||
_set_dynamic_tab(pgAdmin.Browser, !pref['value']);
|
||||
}
|
||||
|
||||
if (!modulesChanged[pref.module]) {
|
||||
modulesChanged[pref.module] = true;
|
||||
}
|
||||
|
||||
requires_refresh = checkRefreshRequired(pref, requires_refresh);
|
||||
|
||||
if (pref.name == 'hide_shared_server') {
|
||||
Notify.confirm(
|
||||
gettext('Browser tree refresh required'),
|
||||
gettext('A browser tree refresh is required. Do you wish to refresh the tree?'),
|
||||
function () {
|
||||
pgAdmin.Browser.tree.destroy({
|
||||
success: function () {
|
||||
pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
},
|
||||
gettext('Refresh'),
|
||||
gettext('Later')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (requires_refresh) {
|
||||
Notify.confirm(
|
||||
gettext('Refresh required'),
|
||||
gettext('A page refresh is required to apply the theme. Do you wish to refresh the page now?'),
|
||||
function () {
|
||||
/* If user clicks Yes */
|
||||
location.reload();
|
||||
return true;
|
||||
},
|
||||
function () { props.closeModal(); /*props.panel.close()*/ },
|
||||
gettext('Refresh'),
|
||||
gettext('Later')
|
||||
);
|
||||
}
|
||||
// Refresh preferences cache
|
||||
pgAdmin.Browser.cache_preferences(modulesChanged);
|
||||
props.closeModal(); /*props.panel.close()*/
|
||||
}).catch((err) => {
|
||||
Notify.alert(err.response.data);
|
||||
});
|
||||
}
|
||||
|
||||
const onDialogHelp = () => {
|
||||
window.open(url_for('help.static', { 'filename': 'preferences.html' }), 'pgadmin_help');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box height={'100%'}>
|
||||
<Box className={classes.root}>
|
||||
<Box className={clsx(classes.preferences)}>
|
||||
<Box className={clsx(classes.treeContainer)} >
|
||||
<Box className={clsx('aciTree', classes.tree)} id={'treeContainer'}></Box>
|
||||
</Box>
|
||||
<Box className={clsx(classes.preferencesContainer)}>
|
||||
{
|
||||
prefSchema.current && loadTree > 0 ?
|
||||
<RightPanel schema={prefSchema.current} initValues={initValues} onDataChange={(changedData) => {
|
||||
Object.keys(changedData).length > 0 ? setDisableSave(false) : setDisableSave(true);
|
||||
prefChangedData.current = changedData;
|
||||
}}></RightPanel>
|
||||
: <></>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className={classes.footer}>
|
||||
<Box>
|
||||
<PgIconButton data-test="dialog-help" onClick={onDialogHelp} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
|
||||
</Box>
|
||||
<Box className={classes.actionBtn} marginLeft="auto">
|
||||
<DefaultButton className={classes.buttonMargin} onClick={() => { props.closeModal(); /*props.panel.close()*/ }} startIcon={<CloseSharpIcon onClick={() => { props.closeModal(); /*props.panel.close()*/ }} />}>
|
||||
{gettext('Cancel')}
|
||||
</DefaultButton>
|
||||
<PrimaryButton className={classes.buttonMargin} startIcon={<SaveSharpIcon />} disabled={disableSave} onClick={() => { savePreferences(prefChangedData, initValues); }}>
|
||||
{gettext('Save')}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{/* </Box> */}
|
||||
|
||||
</Box >
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
PreferencesComponent.propTypes = {
|
||||
schema: PropTypes.array,
|
||||
initValues: PropTypes.object,
|
||||
closeModal: PropTypes.func,
|
||||
renderTree: PropTypes.func
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
// /////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// // pgAdmin 4 - PostgreSQL Tools
|
||||
// //
|
||||
// // Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// // This software is released under the PostgreSQL Licence
|
||||
// //
|
||||
// //////////////////////////////////////////////////////////////
|
||||
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Directory } from 'react-aspen';
|
||||
import { FileTreeX, TreeModelX } from 'pgadmin4-tree';
|
||||
import {Tree} from '../../../../static/js/tree/tree';
|
||||
import { ManagePreferenceTreeNodes } from '../../../../static/js/tree/preference_nodes';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
|
||||
var initPreferencesTree = async (pgBrowser, containerElement, data) => {
|
||||
const MOUNT_POINT = '/preferences';
|
||||
|
||||
// Setup host
|
||||
let ptree = new ManagePreferenceTreeNodes(data);
|
||||
|
||||
// Init Tree with the Tree Parent node '/browser'
|
||||
ptree.init(MOUNT_POINT);
|
||||
|
||||
const host = {
|
||||
pathStyle: 'unix',
|
||||
getItems: async (path) => {
|
||||
return ptree.readNode(path);
|
||||
},
|
||||
sortComparator: (a, b) => {
|
||||
// No nee to sort Query tool options.
|
||||
if (a._parent && a._parent._fileName == 'Query Tool') return 0;
|
||||
// Sort alphabetically
|
||||
if (a.constructor === b.constructor) {
|
||||
return pgAdmin.natural_sort(a.fileName, b.fileName);
|
||||
}
|
||||
let retval = 0;
|
||||
if (a.constructor === Directory) {
|
||||
retval = -1;
|
||||
} else if (b.constructor === Directory) {
|
||||
retval = 1;
|
||||
}
|
||||
return retval;
|
||||
},
|
||||
};
|
||||
|
||||
const pTreeModelX = new TreeModelX(host, MOUNT_POINT);
|
||||
|
||||
const itemHandle = function onReady(handler) {
|
||||
// Initialize preferences Tree
|
||||
pgBrowser.ptree = new Tree(handler, ptree, pgBrowser, 'preferences');
|
||||
return true;
|
||||
};
|
||||
|
||||
await pTreeModelX.root.ensureLoaded();
|
||||
|
||||
// Render Browser Tree
|
||||
await render(
|
||||
<FileTreeX model={pTreeModelX} height={'100%'}
|
||||
onReady={itemHandle} />
|
||||
, containerElement);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initPreferencesTree: initPreferencesTree,
|
||||
};
|
||||
21
web/pgadmin/preferences/static/js/index.js
Normal file
21
web/pgadmin/preferences/static/js/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import pgBrowser from 'top/browser/static/js/browser';
|
||||
import Preferences from './preferences';
|
||||
|
||||
if(!pgAdmin.Preferences) {
|
||||
pgAdmin.Preferences = {};
|
||||
}
|
||||
|
||||
pgAdmin.Preferences = Preferences.getInstance(pgAdmin, pgBrowser);
|
||||
|
||||
module.exports = {
|
||||
Preferences: Preferences,
|
||||
};
|
||||
@@ -7,624 +7,55 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import gettext from 'sources/gettext';
|
||||
import PreferencesComponent from './components/PreferencesComponent';
|
||||
import Notify from '../../../static/js/helpers/Notifier';
|
||||
// import PreferencesTree from './components/PreferencesTree';
|
||||
import { initPreferencesTree } from './components/PreferencesTree';
|
||||
|
||||
define('pgadmin.preferences', [
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
|
||||
'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.backform',
|
||||
'pgadmin.browser', 'sources/modify_animation',
|
||||
'tools/datagrid/static/js/show_query_tool',
|
||||
'sources/tree/pgadmin_tree_save_state',
|
||||
], function(
|
||||
gettext, url_for, $, _, Backbone, Alertify, pgAdmin, Backform, pgBrowser,
|
||||
modifyAnimation, showQueryTool
|
||||
) {
|
||||
// This defines the Preference/Options Dialog for pgAdmin IV.
|
||||
export default class Preferences {
|
||||
static instance;
|
||||
|
||||
/*
|
||||
* Hmm... this module is already been initialized, we can refer to the old
|
||||
* object from here.
|
||||
*/
|
||||
if (pgAdmin.Preferences)
|
||||
return pgAdmin.Preferences;
|
||||
static getInstance(...args) {
|
||||
if (!Preferences.instance) {
|
||||
Preferences.instance = new Preferences(...args);
|
||||
}
|
||||
return Preferences.instance;
|
||||
}
|
||||
|
||||
pgAdmin.Preferences = {
|
||||
init: function() {
|
||||
if (this.initialized)
|
||||
return;
|
||||
constructor(pgAdmin, pgBrowser) {
|
||||
this.pgAdmin = pgAdmin;
|
||||
this.pgBrowser = pgBrowser;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
init() {
|
||||
if (this.initialized)
|
||||
return;
|
||||
this.initialized = true;
|
||||
// Add Preferences in to file menu
|
||||
var menus = [{
|
||||
name: 'mnu_preferences',
|
||||
module: this,
|
||||
applies: ['file'],
|
||||
callback: 'show',
|
||||
enable: true,
|
||||
priority: 3,
|
||||
label: gettext('Preferences'),
|
||||
icon: 'fa fa-cog',
|
||||
}];
|
||||
|
||||
// Declare the Preferences dialog
|
||||
Alertify.dialog('preferencesDlg', function() {
|
||||
this.pgBrowser.add_menus(menus);
|
||||
}
|
||||
|
||||
var jTree, // Variable to create the aci-tree
|
||||
controls = [], // Keep tracking of all the backform controls
|
||||
// created by the dialog.
|
||||
// Dialog containter
|
||||
$container = $('<div class=\'preferences_dialog d-flex flex-row\'></div>');
|
||||
|
||||
|
||||
/*
|
||||
* Preference Model
|
||||
*
|
||||
* This model will be used to keep tracking of the changes done for
|
||||
* an individual option.
|
||||
*/
|
||||
var PreferenceModel = Backbone.Model.extend({
|
||||
idAttribute: 'id',
|
||||
defaults: {
|
||||
id: undefined,
|
||||
value: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* Preferences Collection object.
|
||||
*
|
||||
* We will use only one collection object to keep track of all the
|
||||
* preferences.
|
||||
*/
|
||||
var changed = {},
|
||||
preferences = this.preferences = new(Backbone.Collection.extend({
|
||||
model: PreferenceModel,
|
||||
url: url_for('preferences.index'),
|
||||
updateAll: function() {
|
||||
// We will send only the modified data to the server.
|
||||
for (var key in changed) {
|
||||
this.get(key).save();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}))(null);
|
||||
|
||||
preferences.on('reset', function() {
|
||||
// Reset the changed variables
|
||||
changed = {};
|
||||
});
|
||||
|
||||
preferences.on('change', function(m) {
|
||||
var id = m.get('id'),
|
||||
dependents = m.get('dependents');
|
||||
if (!(id in changed)) {
|
||||
// Keep track of the original value
|
||||
changed[id] = m._previousAttributes.value;
|
||||
} else if (_.isEqual(m.get('value'), changed[id])) {
|
||||
// Remove unchanged models.
|
||||
delete changed[id];
|
||||
}
|
||||
|
||||
// Check dependents exist or not. If exists then call dependentsFound function.
|
||||
if (!_.isNull(dependents) && Array.isArray(dependents) && dependents.length > 0) {
|
||||
dependentsFound(m.get('name'), m.get('value'), dependents);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Function: dependentsFound
|
||||
*
|
||||
* This method will be used to iterate through all the controls and
|
||||
* dependents. If found then perform the appropriate action.
|
||||
*/
|
||||
var dependentsFound = function(pref_name, pref_val, dependents) {
|
||||
// Iterate through all the controls and check the dependents
|
||||
_.each(controls, function(c) {
|
||||
let ctrl_name = c.model.get('name');
|
||||
_.each(dependents, function(deps) {
|
||||
if (ctrl_name === deps) {
|
||||
// Create methods to take appropriate actions and call here.
|
||||
enableDisableMaxWidth(pref_name, pref_val, c);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Function: enableDisableMaxWidth
|
||||
*
|
||||
* This method will be used to enable and disable Maximum Width control
|
||||
*/
|
||||
var enableDisableMaxWidth = function(pref_name, pref_val, control) {
|
||||
if (pref_name === 'column_data_auto_resize' && pref_val === 'by_name') {
|
||||
control.$el.find('input').prop('disabled', true);
|
||||
control.$el.find('input').val(0);
|
||||
} else if (pref_name === 'column_data_auto_resize' && pref_val === 'by_data') {
|
||||
control.$el.find('input').prop('disabled', false);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Function: renderPreferencePanel
|
||||
*
|
||||
* Renders the preference panel in the content div based on the given
|
||||
* preferences.
|
||||
*/
|
||||
var renderPreferencePanel = function(prefs) {
|
||||
/*
|
||||
* Clear the existing html in the preferences content
|
||||
*/
|
||||
var content = $container.find('.preferences_content');
|
||||
|
||||
/*
|
||||
* We should clean up the existing controls.
|
||||
*/
|
||||
if (controls) {
|
||||
_.each(controls, function(c) {
|
||||
if ('$sel' in c) {
|
||||
if (c.$sel.data('select2').isOpen()) c.$sel.data('select2').close();
|
||||
}
|
||||
c.remove();
|
||||
});
|
||||
}
|
||||
content.empty();
|
||||
controls = [];
|
||||
|
||||
/*
|
||||
* We will create new set of controls and render it based on the
|
||||
* list of preferences using the Backform Field, Control.
|
||||
*/
|
||||
_.each(prefs, function(p) {
|
||||
|
||||
var m = preferences.get(p.id);
|
||||
m.errorModel = new Backbone.Model();
|
||||
var f = new Backform.Field(
|
||||
_.extend({}, p, {
|
||||
id: 'value',
|
||||
name: 'value',
|
||||
})
|
||||
),
|
||||
cntr = new(f.get('control'))({
|
||||
field: f,
|
||||
model: m,
|
||||
});
|
||||
content.append(cntr.render().$el);
|
||||
|
||||
// We will keep track of all the controls rendered at the
|
||||
// moment.
|
||||
controls.push(cntr);
|
||||
});
|
||||
|
||||
/* Iterate through all preferences and check if dependents found.
|
||||
* If found then call the dependentsFound method
|
||||
*/
|
||||
_.each(prefs, function(p) {
|
||||
let m = preferences.get(p.id);
|
||||
let dependents = m.get('dependents');
|
||||
if (!_.isNull(dependents) && Array.isArray(dependents) && dependents.length > 0) {
|
||||
dependentsFound(m.get('name'), m.get('value'), dependents);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Function: dialogContentCleanup
|
||||
*
|
||||
* Do the dialog container cleanup on openning.
|
||||
*/
|
||||
|
||||
var dialogContentCleanup = function() {
|
||||
// Remove the existing preferences
|
||||
if (!jTree)
|
||||
return;
|
||||
|
||||
/*
|
||||
* Remove the aci-tree (mainly to remove the jquery object of
|
||||
* aciTree from the system for this container).
|
||||
*/
|
||||
try {
|
||||
jTree.aciTree('destroy');
|
||||
} catch (ex) {
|
||||
// Sometimes - it fails to destroy the tree properly and throws
|
||||
// exception.
|
||||
console.warn(ex.stack || ex);
|
||||
}
|
||||
jTree.off('acitree', treeEventHandler);
|
||||
|
||||
// We need to reset the data from the preferences too
|
||||
preferences.reset();
|
||||
|
||||
/*
|
||||
* Clean up the existing controls.
|
||||
*/
|
||||
if (controls) {
|
||||
_.each(controls, function(c) {
|
||||
c.remove();
|
||||
});
|
||||
}
|
||||
controls = [];
|
||||
|
||||
// Remove all the objects now.
|
||||
$container.empty();
|
||||
},
|
||||
/*
|
||||
* Function: selectFirstCategory
|
||||
*
|
||||
* Whenever a user select a module instead of a category, we should
|
||||
* select the first categroy of it.
|
||||
*/
|
||||
selectFirstCategory = function(api, item) {
|
||||
var data = item ? api.itemData(item) : null;
|
||||
|
||||
if (data && data.preferences) {
|
||||
api.select(item);
|
||||
return;
|
||||
}
|
||||
item = api.first(item);
|
||||
selectFirstCategory(api, item);
|
||||
},
|
||||
/*
|
||||
* A map on how to create controls for each datatype in preferences
|
||||
* dialog.
|
||||
*/
|
||||
getControlMappedForType = function(p) {
|
||||
switch (p.type) {
|
||||
case 'text':
|
||||
return 'input';
|
||||
case 'boolean':
|
||||
p.options = {
|
||||
onText: gettext('True'),
|
||||
offText: gettext('False'),
|
||||
onColor: 'success',
|
||||
offColor: 'ternary',
|
||||
size: 'mini',
|
||||
};
|
||||
return 'switch';
|
||||
case 'node':
|
||||
p.options = {
|
||||
onText: gettext('Show'),
|
||||
offText: gettext('Hide'),
|
||||
onColor: 'success',
|
||||
offColor: 'ternary',
|
||||
size: 'mini',
|
||||
width: '56',
|
||||
};
|
||||
return 'switch';
|
||||
case 'integer':
|
||||
return 'numeric';
|
||||
case 'numeric':
|
||||
return 'numeric';
|
||||
case 'date':
|
||||
return 'datepicker';
|
||||
case 'datetime':
|
||||
return 'datetimepicker';
|
||||
case 'options':
|
||||
var opts = [],
|
||||
has_value = false;
|
||||
// Convert the array to SelectControl understandable options.
|
||||
_.each(p.options, function(o) {
|
||||
if ('label' in o && 'value' in o) {
|
||||
let push_var = {
|
||||
'label': o.label,
|
||||
'value': o.value,
|
||||
};
|
||||
push_var['label'] = o.label;
|
||||
push_var['value'] = o.value;
|
||||
|
||||
if('preview_src' in o) {
|
||||
push_var['preview_src'] = o.preview_src;
|
||||
}
|
||||
opts.push(push_var);
|
||||
if (o.value == p.value)
|
||||
has_value = true;
|
||||
} else {
|
||||
opts.push({
|
||||
'label': o,
|
||||
'value': o,
|
||||
});
|
||||
if (o == p.value)
|
||||
has_value = true;
|
||||
}
|
||||
});
|
||||
if (p.select2 && p.select2.tags == true && p.value && has_value == false) {
|
||||
opts.push({
|
||||
'label': p.value,
|
||||
'value': p.value,
|
||||
});
|
||||
}
|
||||
p.options = opts;
|
||||
return 'select2';
|
||||
case 'select2':
|
||||
var select_opts = [];
|
||||
_.each(p.options, function(o) {
|
||||
if ('label' in o && 'value' in o) {
|
||||
let push_var = {
|
||||
'label': o.label,
|
||||
'value': o.value,
|
||||
};
|
||||
push_var['label'] = o.label;
|
||||
push_var['value'] = o.value;
|
||||
|
||||
if('preview_src' in o) {
|
||||
push_var['preview_src'] = o.preview_src;
|
||||
}
|
||||
select_opts.push(push_var);
|
||||
} else {
|
||||
select_opts.push({
|
||||
'label': o,
|
||||
'value': o,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
p.options = select_opts;
|
||||
return 'select2';
|
||||
|
||||
case 'multiline':
|
||||
return 'textarea';
|
||||
case 'switch':
|
||||
return 'switch';
|
||||
case 'keyboardshortcut':
|
||||
return 'keyboardShortcut';
|
||||
case 'radioModern':
|
||||
return 'radioModern';
|
||||
case 'selectFile':
|
||||
return 'binary-paths-grid';
|
||||
case 'threshold':
|
||||
p.warning_label = gettext('Warning');
|
||||
p.alert_label = gettext('Alert');
|
||||
p.unit = gettext('(in minutes)');
|
||||
return 'threshold';
|
||||
default:
|
||||
if (console && console.warn) {
|
||||
// Warning for developer only.
|
||||
console.warn(
|
||||
'Hmm.. We don\'t know how to render this type - \'\'' + p.type + '\' of control.'
|
||||
);
|
||||
}
|
||||
return 'input';
|
||||
}
|
||||
},
|
||||
/*
|
||||
* function: treeEventHandler
|
||||
*
|
||||
* It is basically a callback, which listens to aci-tree events,
|
||||
* and act accordingly.
|
||||
*
|
||||
* + Selection of the node will existance of the preferences for
|
||||
* the selected tree-node, if not pass on to select the first
|
||||
* category under a module, else pass on to the render function.
|
||||
*
|
||||
* + When a new node is added in the tree, it will add the relavent
|
||||
* preferences in the preferences model collection, which will be
|
||||
* called during initialization itself.
|
||||
*
|
||||
*
|
||||
*/
|
||||
treeEventHandler = function(event, api, item, eventName) {
|
||||
// Look for selected item (if none supplied)!
|
||||
item = item || api.selected();
|
||||
|
||||
// Event tree item has itemData
|
||||
var d = item ? api.itemData(item) : null;
|
||||
|
||||
/*
|
||||
* boolean (switch/checkbox), string, enum (combobox - enumvals),
|
||||
* integer (min-max), font, color
|
||||
*/
|
||||
switch (eventName) {
|
||||
case 'selected':
|
||||
if (!d)
|
||||
break;
|
||||
|
||||
if (d.preferences) {
|
||||
/*
|
||||
* Clear the existing html in the preferences content
|
||||
*/
|
||||
$container.find('.preferences_content');
|
||||
|
||||
renderPreferencePanel(d.preferences);
|
||||
|
||||
break;
|
||||
} else {
|
||||
selectFirstCategory(api, item);
|
||||
}
|
||||
break;
|
||||
case 'added':
|
||||
if (!d)
|
||||
break;
|
||||
|
||||
// We will add the preferences in to the preferences data
|
||||
// collection.
|
||||
if (d.preferences && _.isArray(d.preferences)) {
|
||||
_.each(d.preferences, function(p) {
|
||||
preferences.add({
|
||||
'id': p.id,
|
||||
'value': p.value,
|
||||
'category_id': d.id,
|
||||
'mid': d.mid,
|
||||
'name': p.name,
|
||||
'dependents': p.dependents,
|
||||
});
|
||||
/*
|
||||
* We don't know until now, how to render the control for
|
||||
* this preference.
|
||||
*/
|
||||
if (!p.control) {
|
||||
p.control = getControlMappedForType(p);
|
||||
}
|
||||
if (p.help_str) {
|
||||
p.helpMessage = p.help_str;
|
||||
}
|
||||
});
|
||||
}
|
||||
d.sortable = false;
|
||||
break;
|
||||
case 'loaded':
|
||||
// Let's select the first category from the prefrences.
|
||||
// We need to wait for sometime before all item gets loaded
|
||||
// properly.
|
||||
setTimeout(
|
||||
function() {
|
||||
selectFirstCategory(api, null);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Dialog property
|
||||
return {
|
||||
main: function() {
|
||||
|
||||
// Remove the existing content first.
|
||||
dialogContentCleanup();
|
||||
|
||||
$container.append(
|
||||
'<div class=\'pg-el-sm-3 preferences_tree aciTree\'></div>'
|
||||
).append(
|
||||
'<div class=\'pg-el-sm-9 preferences_content\'>' +
|
||||
gettext('Category is not selected.') +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
// Create the aci-tree for listing the modules and categories of
|
||||
// it.
|
||||
jTree = $container.find('.preferences_tree');
|
||||
jTree.on('acitree', treeEventHandler);
|
||||
|
||||
jTree.aciTree({
|
||||
selectable: true,
|
||||
expand: true,
|
||||
fullRow: true,
|
||||
ajax: {
|
||||
url: url_for('preferences.index'),
|
||||
},
|
||||
animateRoot: true,
|
||||
unanimated: false,
|
||||
show: {duration: 75},
|
||||
hide: {duration: 75},
|
||||
view: {duration: 75},
|
||||
});
|
||||
|
||||
if (jTree.aciTree('api')) modifyAnimation.modifyAcitreeAnimation(pgBrowser, jTree.aciTree('api'));
|
||||
|
||||
this.show();
|
||||
},
|
||||
setup: function() {
|
||||
return {
|
||||
buttons: [{
|
||||
text: '',
|
||||
key: 112,
|
||||
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
|
||||
attrs: {
|
||||
name: 'dialog_help',
|
||||
type: 'button',
|
||||
label: gettext('Preferences'),
|
||||
'aria-label': gettext('Help'),
|
||||
url: url_for(
|
||||
'help.static', {
|
||||
'filename': 'preferences.html',
|
||||
}
|
||||
),
|
||||
},
|
||||
}, {
|
||||
text: gettext('Cancel'),
|
||||
key: 27,
|
||||
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
|
||||
}, {
|
||||
text: gettext('Save'),
|
||||
key: 13,
|
||||
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
|
||||
}],
|
||||
focus: {
|
||||
element: 0,
|
||||
},
|
||||
options: {
|
||||
padding: !1,
|
||||
overflow: !1,
|
||||
title: gettext('Preferences'),
|
||||
closableByDimmer: false,
|
||||
modal: true,
|
||||
pinnable: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
callback: function(e) {
|
||||
if (e.button.element.name == 'dialog_help') {
|
||||
e.cancel = true;
|
||||
pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
|
||||
null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button.text == gettext('Save')) {
|
||||
let requires_refresh = false;
|
||||
preferences.updateAll();
|
||||
|
||||
/* Find the modules changed */
|
||||
let modulesChanged = {};
|
||||
_.each(changed, (val, key)=> {
|
||||
let pref = pgBrowser.get_preference_for_id(Number(key));
|
||||
|
||||
if(pref['name'] == 'dynamic_tabs') {
|
||||
showQueryTool._set_dynamic_tab(pgBrowser, !pref['value']);
|
||||
}
|
||||
|
||||
if(!modulesChanged[pref.module]) {
|
||||
modulesChanged[pref.module] = true;
|
||||
}
|
||||
|
||||
if(pref.name == 'theme') {
|
||||
requires_refresh = true;
|
||||
}
|
||||
|
||||
if(pref.name == 'hide_shared_server') {
|
||||
Notify.confirm(
|
||||
gettext('Browser tree refresh required'),
|
||||
gettext('A browser tree refresh is required. Do you wish to refresh the tree?'),
|
||||
function() {
|
||||
pgAdmin.Browser.tree.destroy({
|
||||
success: function() {
|
||||
pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
},
|
||||
function() {
|
||||
preferences.reset();
|
||||
changed = {};
|
||||
return true;
|
||||
},
|
||||
gettext('Refresh'),
|
||||
gettext('Later')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if(requires_refresh) {
|
||||
Notify.confirm(
|
||||
gettext('Refresh required'),
|
||||
gettext('A page refresh is required to apply the theme. Do you wish to refresh the page now?'),
|
||||
function() {
|
||||
/* If user clicks Yes */
|
||||
location.reload();
|
||||
return true;
|
||||
},
|
||||
function() {/* If user clicks No */ return true;},
|
||||
gettext('Refresh'),
|
||||
gettext('Later')
|
||||
);
|
||||
}
|
||||
// Refresh preferences cache
|
||||
pgBrowser.cache_preferences(modulesChanged);
|
||||
}
|
||||
},
|
||||
build: function() {
|
||||
this.elements.content.appendChild($container.get(0));
|
||||
Alertify.pgDialogBuild.apply(this);
|
||||
},
|
||||
hooks: {
|
||||
onshow: function() {/* This is intentional (SonarQube) */},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
show: function() {
|
||||
Alertify.preferencesDlg(true).resizeTo(pgAdmin.Browser.stdW.calc(pgAdmin.Browser.stdW.lg),pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg));
|
||||
},
|
||||
};
|
||||
|
||||
return pgAdmin.Preferences;
|
||||
});
|
||||
// This is a callback function to show preferences.
|
||||
show() {
|
||||
// Render Preferences component
|
||||
Notify.showModal(gettext('Preferences'), (closeModal) => {
|
||||
return <PreferencesComponent
|
||||
renderTree={(prefTreeData) => {
|
||||
initPreferencesTree(this.pgBrowser, document.getElementById('treeContainer'), prefTreeData);
|
||||
}} closeModal={closeModal} />;
|
||||
}, { isFullScreen: false, isResizeable: true, showFullScreen: true, isFullWidth: true, dialogWidth: 900, dialogHeight: 550 });
|
||||
}
|
||||
}
|
||||
|
||||
8
web/pgadmin/preferences/tests/__init__.py
Normal file
8
web/pgadmin/preferences/tests/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
27
web/pgadmin/preferences/tests/preferences_test_data.json
Normal file
27
web/pgadmin/preferences/tests/preferences_test_data.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"get_preferences": [
|
||||
{
|
||||
"name": "Get the all Preferences",
|
||||
"url": "/preferences/",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"mock_data": {
|
||||
},
|
||||
"expected_data": {
|
||||
"status_code": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"update_preferences": [
|
||||
{
|
||||
"name": "Update the Preferences",
|
||||
"url": "/preferences/",
|
||||
"is_positive_test": true,
|
||||
"mocking_required": false,
|
||||
"mock_data": {},
|
||||
"expected_data": {
|
||||
"status_code": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
39
web/pgadmin/preferences/tests/test_preferences_get.py
Normal file
39
web/pgadmin/preferences/tests/test_preferences_get.py
Normal file
@@ -0,0 +1,39 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import os
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression.test_setup import config_data
|
||||
import json
|
||||
import config
|
||||
|
||||
test_user_details = None
|
||||
if config.SERVER_MODE:
|
||||
test_user_details = config_data['pgAdmin4_test_non_admin_credentials']
|
||||
|
||||
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
with open(CURRENT_PATH + "/preferences_test_data.json") as data_file:
|
||||
test_cases = json.load(data_file)
|
||||
|
||||
|
||||
class GetPreferencesTest(BaseTestGenerator):
|
||||
"""
|
||||
This class will fetch all Preferences
|
||||
"""
|
||||
|
||||
scenarios = utils.generate_scenarios('get_preferences', test_cases)
|
||||
|
||||
def runTest(self):
|
||||
self.get_preferences()
|
||||
|
||||
def get_preferences(self):
|
||||
response = self.tester.get(self.url,
|
||||
content_type='html/json')
|
||||
self.assertTrue(response.status_code, 200)
|
||||
60
web/pgadmin/preferences/tests/test_preferences_update.py
Normal file
60
web/pgadmin/preferences/tests/test_preferences_update.py
Normal file
@@ -0,0 +1,60 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import os
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression.test_setup import config_data
|
||||
from regression import parent_node_dict
|
||||
import json
|
||||
import config
|
||||
|
||||
test_user_details = None
|
||||
if config.SERVER_MODE:
|
||||
test_user_details = config_data['pgAdmin4_test_non_admin_credentials']
|
||||
|
||||
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
with open(CURRENT_PATH + "/preferences_test_data.json") as data_file:
|
||||
test_cases = json.load(data_file)
|
||||
|
||||
|
||||
class GetPreferencesTest(BaseTestGenerator):
|
||||
"""
|
||||
This class will fetch all Preferences
|
||||
"""
|
||||
|
||||
scenarios = utils.generate_scenarios('update_preferences', test_cases)
|
||||
|
||||
def setUp(self):
|
||||
response = self.tester.get(self.url,
|
||||
content_type='html/json')
|
||||
self.assertTrue(response.status_code, 200)
|
||||
parent_node_dict['preferences'] = response.data
|
||||
|
||||
def runTest(self):
|
||||
self.update_preferences()
|
||||
|
||||
def update_preferences(self):
|
||||
if 'preferences' in parent_node_dict:
|
||||
data = \
|
||||
json.loads(parent_node_dict['preferences'])[0]['children'][0][
|
||||
'preferences'][0]
|
||||
updated_data = [{
|
||||
'id': data['id'],
|
||||
'category_id': data['cid'],
|
||||
'mid': data['mid'],
|
||||
'name': data['name'],
|
||||
'value': not data['value']
|
||||
}]
|
||||
response = self.tester.put(self.url,
|
||||
data=json.dumps(updated_data),
|
||||
content_type='html/json')
|
||||
self.assertTrue(response.status_code, 200)
|
||||
else:
|
||||
self.fail('Preferences not found')
|
||||
Reference in New Issue
Block a user