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
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user