///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // // Copyright (C) 2013 - 2021, The pgAdmin Development Team // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// define([ 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'codemirror', 'sources/sqleditor_utils', 'sources/keyboard_shortcuts', 'sources/window', 'sources/select2/configure_show_on_scroll', 'color-picker', 'pgadmin.backgrid', 'select2', 'bootstrap.toggle', ], function(gettext, _, $, Backbone, Backform, Backgrid, CodeMirror, SqlEditorUtils, keyboardShortcuts, pgWindow, configure_show_on_scroll, Pickr) { var pgAdmin = (window.pgAdmin = window.pgAdmin || {}), pgBrowser = pgAdmin.Browser; pgAdmin.editableCell = function() { if (this.attributes && !_.isUndefined(this.attributes.disabled) && !_.isNull(this.attributes.disabled)) { if (_.isFunction(this.attributes.disabled)) { return !(this.attributes.disabled.apply(this, arguments)); } if (_.isBoolean(this.attributes.disabled)) { return !this.attributes.disabled; } } }; // HTML markup global class names. More can be added by individual controls // using _.extend. Look at RadioControl as an example. _.extend(Backform, { controlLabelClassName: 'control-label pg-el-sm-3 pg-el-12', controlsClassName: 'pgadmin-controls pg-el-sm-9 pg-el-12', controlContainerClassName: 'pgadmin-controls pg-el-sm-9 pg-el-12', groupClassName: 'pgadmin-control-group form-group row pg-el-12', setGroupClassName: 'set-group pg-el-12', tabClassName: 'backform-tab pg-el-12', setGroupContentClassName: 'fieldset-content pg-el-12', accordianGroupClassName: 'accordian-group pg-el-12', accordianContentClassName: 'accordian-content pg-el-12', hiddenClassName: 'd-none', helpMessageClassName: 'form-text text-muted help-block', helpBlockControlClass: 'pgadmin-controls offset-sm-3 pg-el-sm-9 pg-el-12', }); Backform.controlMapper = { 'int': ['uneditable-input', 'numeric', 'integer'], 'text': ['uneditable-input', 'input', 'string'], 'numeric': ['uneditable-input', 'numeric', 'numeric'], 'date': 'datepicker', 'datetime': 'datetimepicker', 'boolean': 'boolean', 'options': ['readonly-option', 'select', Backgrid.Extension.PGSelectCell], 'multiline': ['textarea', 'textarea', 'string'], 'collection': ['sub-node-collection', 'sub-node-collection', 'string'], 'uniqueColCollection': ['unique-col-collection', 'unique-col-collection', 'string'], 'switch': 'switch', 'select2': 'select2', 'note': 'note', 'color': 'color', 'radioModern': 'radioModern', }; Backform.getMappedControl = function(type, mode) { if (type in Backform.controlMapper) { var m = Backform.controlMapper[type]; if (!_.isArray(m)) { return m; } var idx = 1, len = _.size(m); switch (mode) { case 'properties': idx = 0; break; case 'edit': case 'create': case 'control': idx = 1; break; case 'cell': idx = 2; break; default: idx = 0; break; } return m[idx > len ? 0 : idx]; } return type; }; /* Returns raw data as it is */ var RawFormatter = Backform.RawFormatter = function() {}; _.extend(RawFormatter.prototype, { fromRaw: function(rawData) { return rawData; }, toRaw: function(formattedData) { return formattedData; }, }); var BackformControlInit = Backform.Control.prototype.initialize, BackformControlRemove = Backform.Control.prototype.remove; // Override the Backform.Control to allow to track changes in dependencies, // and rerender the View element _.extend(Backform.Control.prototype, { defaults: _.extend(Backform.Control.prototype.defaults, { helpMessage: null, }), initialize: function() { BackformControlInit.apply(this, arguments); // Listen to the dependent fields in the model for any change var deps = this.field.get('deps'); var self = this; if (deps && _.isArray(deps)) { _.each(deps, function(d) { var attrArr = d.split('.'); var name = attrArr.shift(); self.listenTo(self.model, 'change:' + name, self.render); }); } }, remove: function() { // Remove the events for the dependent fields in the model var self = this, deps = self.field.get('deps'); var attrArr = this.field.get('name').split('.'); var name = attrArr.shift(); self.stopListening(self.model, 'change:' + name, self.render); if (self.model.errorModel instanceof Backbone.Model) { self.stopListening( self.model.errorModel, 'change:' + name, self.updateInvalid ); } if (deps && _.isArray(deps)) { _.each(deps, function(d) { var attrArray = d.split('.'); var depname = attrArray.shift(); self.stopListening(self.model, 'change:' + depname, self.render); }); } if (this.cleanup) { this.cleanup.apply(this); } if (BackformControlRemove) { BackformControlRemove.apply(self, arguments); } else { Backbone.View.prototype.remove.apply(self, arguments); } }, template: _.template([ '', '
', ' <%=readonly ? "readonly aria-readonly=true" : ""%> id="<%=cId%>" value="<%-value%>" />', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), clearInvalid: function() { this.$el.removeClass(Backform.errorClassName); this.$el.find('.pgadmin-control-error-message').remove(); return this; }, updateInvalid: function() { var self = this, errorModel = this.model.errorModel; if (!(errorModel instanceof Backbone.Model)) return this; this.clearInvalid(); /* * Find input which have name attribute. */ this.$el.find(':input[name]').not('button').each(function(ix, el) { var error = self.keyPathAccessor( errorModel.toJSON(), $(el).attr('name') ); if (_.isEmpty(error)) return; self.$el.addClass(Backform.errorClassName); }); }, /* * Overriding the render function of the control to allow us to eval the * values properly. */ render: function() { var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), evalF = function(f, d, m) { return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f); }; // Evaluate the disabled, visible, and required option _.extend(data, { disabled: evalF(data.disabled, data, this.model), readonly: evalF(data.readonly, data, this.model), visible: evalF(data.visible, data, this.model), required: evalF(data.required, data, this.model), }); data.cId = data.cId || _.uniqueId('pgC_'); // Clean up first this.$el.removeClass(Backform.hiddenClassName); if (!data.visible) this.$el.addClass(Backform.hiddenClassName); this.$el.html(this.template(data)).addClass(field.name); this.updateInvalid(); return this; }, }); /* * Override the input control events in order to reslove the issue related to * not updating the value sometimes in the input control. */ _.extend( Backform.InputControl.prototype, { defaults: _.extend(Backform.InputControl.prototype.defaults, { controlLabelClassName: Backform.controlLabelClassName, controlsClassName: Backform.controlsClassName, }), template: _.template([ '', '
', ' <%=readonly ? "readonly aria-readonly=true" : ""%> <%=required ? "required" : ""%> />', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), events: { 'change input': 'onChange', 'blur input': 'onChange', 'keyup input': 'onKeyUp', 'focus input': 'clearInvalid', }, onKeyUp: function(ev) { if (this.key_timeout) { clearTimeout(this.key_timeout); } this.keyup_timeout = setTimeout(function() { this.onChange(ev); }.bind(this), 400); }, }); /* * Override the textarea control events in order to resolve the issue related * to not updating the value in model on certain browsers in few situations * like copy/paste, deletion using backspace. * * Reference: * http://stackoverflow.com/questions/11338592/how-can-i-bind-to-the-change-event-of-a-textarea-in-jquery */ _.extend( Backform.TextareaControl.prototype, { defaults: _.extend( Backform.TextareaControl.prototype.defaults, { rows: 5, helpMessage: null, maxlength: null, } ), events: { 'change textarea': 'onChange', 'keyup textarea': 'onKeyUp', 'paste textarea': 'onChange', 'selectionchange textarea': 'onChange', 'focus textarea': 'clearInvalid', }, template: _.template([ '', '
', ' ', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), onKeyUp: function(ev) { if (this.key_timeout) { clearTimeout(this.key_timeout); } this.keyup_timeout = setTimeout(function() { this.onChange(ev); }.bind(this), 400); }, }); /* * Overriding the render function of the select control to allow us to use * options as function, which should return array in the format of * (label, value) pair. */ Backform.SelectControl.prototype.render = function() { var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), evalF = function(f, d, m) { return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f); }; // Evaluate the disabled, visible, and required option _.extend(data, { disabled: evalF(data.disabled, data, this.model), visible: evalF(data.visible, data, this.model), required: evalF(data.required, data, this.model), }); // Evaluation the options if (_.isFunction(data.options)) { try { data.options = data.options(this); } catch (e) { // Do nothing data.options = []; this.model.trigger('pgadmin-view:transform:error', this.model, this.field, e); } } data.cId = data.cId || _.uniqueId('pgC_'); // Clean up first this.$el.removeClass(Backform.hiddenClassName); if (!data.visible) this.$el.addClass(Backform.hiddenClassName); this.$el.html(this.template(data)).addClass(field.name); this.updateInvalid(); return this; }; Backform.SelectControl.prototype.template = _.template([ '', '
', ' ', '
', ].join('\n')); _.extend(Backform.SelectControl.prototype.defaults, { helpMessage: null, }); Backform.ReadonlyOptionControl = Backform.SelectControl.extend({ template: _.template([ '', '
', '<% for (var i=0; i < options.length; i++) { %>', ' <% var option = options[i]; %>', ' <% if (option.value === rawValue) { %>', ' ', ' <% } %>', '<% } %>', '<% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', '<% } %>', '
', ].join('\n')), events: {}, getValueFromDOM: function() { return this.formatter.toRaw(this.$el.find('span').text(), this.model); }, }); /* * Override the function 'updateInvalid' of the radio control to resolve an * issue, which will not render the error block multiple times for each * options. */ _.extend( Backform.RadioControl.prototype, { updateInvalid: function() { var self = this, errorModel = this.model.errorModel; if (!(errorModel instanceof Backbone.Model)) return this; this.clearInvalid(); /* * Find input which have name attribute. */ this.$el.find(':input[name]').not('button').each(function(ix, el) { var error = self.keyPathAccessor( errorModel.toJSON(), $(el).attr('name') ); if (_.isEmpty(error)) return; self.$el.addClass(Backform.errorClassName).find( '[type="radio"]' ).append( $('
').addClass( 'pgadmin-control-error-message pg-el-offset-4 pg-el-8 pg-el-8 help-block' ).text(error)); }); }, }); Backform.RadioModernControl = Backform.RadioControl.extend({ defaults: { controlLabelClassName: Backform.controlLabelClassName, controlsClassName: Backform.controlsClassName, extraClasses: [], helpMessage: '', name: '', }, events: _.extend({}, Backform.InputControl.prototype.events, { 'click label.btn': 'toggle', }), toggle: function(e) { /* Toggle the other buttons to unchecked and current to checked */ let $curr = $(e.currentTarget), $btn_group = $curr.closest('.btn-group'); $btn_group.find('.btn') .removeClass('btn-primary') .addClass('btn-secondary') .find('.fa') .addClass('visibility-hidden') .siblings('input') .prop('checked', false); $curr.removeClass('btn-secondary') .addClass('btn-primary') .find('.fa') .removeClass('visibility-hidden') .siblings('input') .prop('checked', true) .trigger('change'); e.preventDefault(); e.stopPropagation(); }, template: _.template([ '<% if (label) { %>', '', '<% } %>', '
', '
aria-labelledby="<%=cId%>_grplabel" <%}%>>', ' <% for (var i=0; i < options.length; i++) { %>', ' <% var option = options[i]; %>', ' ', ' <% } %>', '
', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), formatter: RawFormatter, getValueFromDOM: function() { return this.formatter.toRaw(this.$el.find('input[type="radio"]:checked').attr('value'), this.model); }, render: function() { var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), // Evaluate the disabled, visible, and required option evalF = function evalF(f, d, m) { return _.isFunction(f) ? !!f.apply(d, [m]) : !!f; }; _.extend(data, { disabled: evalF(data.disabled, data, this.model), visible: evalF(data.visible, data, this.model), required: evalF(data.required, data, this.model), }); // Clean up first data.cId = data.cId || _.uniqueId('pgC_'); data.options = _.isFunction(data.options) ? data.options.apply(data, [this.model]) : data.options; this.$el.removeClass(Backform.hiddenClassName); if (!data.visible) this.$el.addClass(Backform.hiddenClassName); this.$el.html(this.template(data)).addClass(field.name); this.updateInvalid(); this.$el.find('.btn').on('keyup', (e)=>{ /* Spacebar click */ if (e.keyCode == 32) { $(e.currentTarget).trigger('click'); } }); return this; }, }); // Requires the Bootstrap Switch to work. Backform.SwitchControl = Backform.InputControl.extend({ defaults: { label: '', options: { onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'ternary', size: 'mini', width: null, height: null, }, controlLabelClassName: Backform.controlLabelClassName, controlsClassName: Backform.controlsClassName, extraClasses: [], helpMessage: null, extraToggleClasses: null, }, template: _.template([ '<%=label%>', '
', ' <%=disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%> <%=required ? "required" : ""%> />', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), getValueFromDOM: function() { return this.formatter.toRaw( this.$input.prop('checked'), this.model ); }, events: { 'change input': 'onChange', 'keyup': 'toggleSwitch', }, setSrValue: function() { let {onText, offText} = _.defaults(this.field.get('options'), this.defaults.options); let label = this.field.get('label'); if(this.$el.find('.toggle.btn').hasClass('off')) { this.$el.find('.sr-value').text(` ${label}, ${offText}, ` + gettext('Toggle') + ` `); } else { this.$el.find('.sr-value').text(` ${label}, ${onText}, ` + gettext('Toggle') + ` `); } }, onChange: function() { Backform.InputControl.prototype.onChange.apply(this, arguments); this.setSrValue(); }, toggleSwitch: function(e) { if (e.keyCode == 32) { this.$el.find('input[type=checkbox]').bootstrapToggle('toggle'); e.preventDefault(); this.setSrValue(); } }, render: function() { var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), evalF = function(f, d, m) { return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f); }; // Evaluate the disabled, visible, and required option _.extend(data, { disabled: evalF(field.disabled, field, this.model), readonly: evalF(field.readonly, field, this.model), visible: evalF(data.visible, field, this.model), required: evalF(data.required, field, this.model), }); data.cId = data.cId || _.uniqueId('pgC_'); // Clean up first this.$el.removeClass(Backform.hiddenClassName); if (!data.visible) this.$el.addClass(Backform.hiddenClassName); if(Backform.requiredInputClassName) { this.$el.removeClass(Backform.requiredInputClassName); } if (data.required) { this.$el.addClass(Backform.requiredInputClassName); } /* Set disabled for both disabled and readonly */ data.options = _.defaults({ disabled: data.disabled || data.readonly, }, this.field.get('options'), this.defaults.options, $.fn.bootstrapToggle.defaults); this.$el.html(this.template(data)).addClass(field.name); // Add Extra Toggle classes to render multiple toggles in a single row if(!_.isNull(field.extraToggleClasses)) this.$el.addClass(field.extraToggleClasses); this.$input = this.$el.find('input[type=checkbox]').first(); this.$input.bootstrapToggle(); // When disable then set tabindex value to -1 this.$el.find('.toggle.btn') .attr('tabindex', data.disabled ? '-1' : '0') .attr('id', data.cId); this.$el.find('.toggle.btn .toggle-group .btn').attr('aria-hidden', true); this.$el.find('div.toggle').append(''); this.setSrValue(); this.updateInvalid(); /* Bootstrap toggle does not have option for readonly * If readonly, then let it focus. */ if(data.readonly) { this.$el.find('.select2-selection').attr('tabindex', 0); } return this; }, }); // Backform Dialog view (in bootstrap tabbular form) // A collection of field models. Backform.Dialog = Backform.Form.extend({ /* Array of objects having attributes [label, fields] */ schema: undefined, tagName: 'div', legend: true, className: function() { return 'pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12'; }, tabPanelClassName: function() { return Backform.tabClassName; }, tabIndex: 0, initialize: function(opts) { var s = opts.schema; if (s && _.isArray(s)) { this.schema = _.each(s, function(o) { if (o.fields && !(o.fields instanceof Backbone.Collection)) o.fields = new Backform.Fields(o.fields); o.cId = o.cId || _.uniqueId('pgC_'); o.hId = o.hId || _.uniqueId('pgH_'); o.disabled = o.disabled || false; o.legend = opts.legend; }); if (opts.tabPanelClassName && _.isFunction(opts.tabPanelClassName)) { this.tabPanelClassName = opts.tabPanelClassName; } } this.model.errorModel = opts.errorModel || this.model.errorModel || new Backbone.Model(); this.controls = []; }, template: { 'header': _.template([ '', ].join(' ')), 'panel': _.template( '
' ), }, render: function() { this.cleanup(); var c = this.$el .children().first().children('.active') .first().attr('id'), m = this.model, controls = this.controls, tmpls = this.template, self = this, idx = (this.tabIndex * 100), evalF = function(f, d, model) { return (_.isFunction(f) ? !!f.apply(d, [model]) : !!f); }; this.$el .empty() .attr('role', 'tabpanel') .attr('class', _.result(this, 'tabPanelClassName')); m.panelEl = this.$el; var tabHead = $('') .appendTo(this.$el); var tabContent = $('
') .appendTo(this.$el); _.each(this.schema, function(o) { idx++; if (!o.version_compatible || !evalF(o.visible, o, m)) { return; } var el = $((tmpls['panel'])(_.extend(o, { 'tabIndex': idx, 'tabPanelCodeClass': o.tabPanelCodeClass ? o.tabPanelCodeClass : '', }))) .appendTo(tabContent) .removeClass('collapse').addClass('collapse'); $((tmpls['header'])(o)).appendTo(tabHead); o.fields.each(function(f) { var cntr = new(f.get('control'))({ field: f, model: m, dialog: self, tabIndex: idx, }); el.append(cntr.render().$el); controls.push(cntr); }); tabHead.find('a[data-toggle="tab"]').off( 'shown.bs.tab' ).off('hidden.bs.tab').on( 'hidden.bs.tab', function() { self.hidden_tab = $(this).data('tabIndex'); }).on('shown.bs.tab', function() { var ctx = this; ctx.shown_tab = $(ctx).data('tabIndex'); m.trigger('pg-property-tab-changed', { 'model': m, 'shown': ctx.shown_tab, 'hidden': ctx.hidden_tab, 'tab': ctx, }); }); }); var makeActive = tabHead.find('[id="' + c + '"]').first(); if (makeActive.length == 1) { makeActive.addClass('active show'); tabContent.find('#' + makeActive.attr('aria-controls')) .addClass('active show'); } else { tabHead.find('.nav-link').first().addClass('active show'); tabContent.find('.tab-pane').first().addClass('active show'); } return this; }, remove: function(opts) { if (opts && opts.data) { if (this.model) { if (this.model.reset) { this.model.reset({ validate: false, silent: true, stop: true, }); } this.model.clear({ validate: false, silent: true, stop: true, }); delete(this.model); } if (this.errorModel) { this.errorModel.clear({ validate: false, silent: true, stop: true, }); delete(this.errorModel); } } this.cleanup(); Backform.Form.prototype.remove.apply(this, arguments); }, }); Backform.Accordian = Backform.Dialog.extend({ className: function() { return 'set-group pg-el-12'; }, tabPanelClassName: function() { return Backform.tabClassName; }, legendClass: 'badge', contentClass: Backform.accordianContentClassName + ' collapse show', template: { 'header': _.template([ '
>', ' <% if (legend != false) { %>', '
data-target="#<%=cId%>" aria-controls="<%=cId%>" aria-level="3" role="heading"><%=collapse ? "" : "" %><%=label%>', ' <% } %>', '
', ].join('\n')), 'content': _.template( '
' ), }, collapse: true, render: function() { this.cleanup(); var m = this.model, $el = this.$el, tmpl = this.template, controls = this.controls, data = { 'className': _.result(this, 'className'), 'legendClass': _.result(this, 'legendClass'), 'contentClass': _.result(this, 'contentClass'), 'collapse': _.result(this, 'collapse'), }, idx = (this.tabIndex * 100), evalF = function(f, d, model) { return (_.isFunction(f) ? !!f.apply(d, [model]) : !!f); }; this.$el.empty(); _.each(this.schema, function(o) { idx++; if (!o.version_compatible || !evalF(o.visible, o, m)) { return; } if (!o.fields) return; var d = _.extend({}, data, o), h = $((tmpl['header'])(d)).appendTo($el), el = $((tmpl['content'])(d)).appendTo(h); o.fields.each(function(f) { var cntr = new(f.get('control'))({ field: f, model: m, tabIndex: idx, }); el.append(cntr.render().$el); controls.push(cntr); }); }); return this; }, getValueFromDOM: function() { return ''; }, events: {}, }); Backform.Fieldset = Backform.Dialog.extend({ className: function() { return 'set-group pg-el-12'; }, tabPanelClassName: function() { return Backform.tabClassName; }, fieldsetClass: Backform.setGroupClassName, legendClass: 'badge', contentClass: Backform.setGroupContentClassName + ' collapse show', template: { 'header': _.template([ '
>', ' <% if (legend != false) { %>', ' data-target="#<%=cId%>"><%=collapse ? "" : "" %><%=label%>', ' <% } %>', '
', ].join('\n')), 'content': _.template( '
' ), }, collapse: true, render: function() { this.cleanup(); var m = this.model, $el = this.$el, tmpl = this.template, controls = this.controls, data = { 'className': _.result(this, 'className'), 'fieldsetClass': _.result(this, 'fieldsetClass'), 'legendClass': _.result(this, 'legendClass'), 'contentClass': _.result(this, 'contentClass'), 'collapse': _.result(this, 'collapse'), }, idx = (this.tabIndex * 100), evalF = function(f, d, model) { return (_.isFunction(f) ? !!f.apply(d, [model]) : !!f); }; this.$el.empty(); _.each(this.schema, function(o) { idx++; if (!o.version_compatible || !evalF(o.visible, o, m)) { return; } if (!o.fields) return; var d = _.extend({}, data, o), h = $((tmpl['header'])(d)).appendTo($el), el = $((tmpl['content'])(d)).appendTo(h); o.fields.each(function(f) { var cntr = new(f.get('control'))({ field: f, model: m, tabIndex: idx, }); el.append(cntr.render().$el); controls.push(cntr); }); }); return this; }, getValueFromDOM: function() { return ''; }, events: {}, }); Backform.generateGridColumnsFromModel = function(node_info, m, type, cols, node) { var groups = Backform.generateViewSchema( node_info, m, type, node, true, true ), schema = [], columns = [], func, idx = 0; // Create another array if cols is of type object & store its keys in that array, // If cols is object then chances that we have custom width class attached with in. if (_.isNull(cols) || _.isUndefined(cols)) { func = function(f) { f.cell_priority = idx; idx = idx + 1; // We can also provide custom header cell class in schema itself, // But we will give priority to extraClass attached in cols // If headerCell property is already set by cols then skip extraClass property from schema if (!(f.headerCell) && f.cellHeaderClasses) { f.headerCell = Backgrid.Extension.CustomHeaderCell; } }; } else if (_.isArray(cols)) { func = function(f) { f.cell_priority = _.indexOf(cols, f.name); // We can also provide custom header cell class in schema itself, // But we will give priority to extraClass attached in cols // If headerCell property is already set by cols then skip extraClass property from schema if ((!f.headerCell) && f.cellHeaderClasses) { f.headerCell = Backgrid.Extension.CustomHeaderCell; } }; } else if (_.isObject(cols)) { var tblCols = Object.keys(cols); func = function(f) { var val = (f.name in cols) && cols[f.name], i; if (_.isNull(val) || _.isUndefined(val)) { f.cell_priority = -1; return; } if (_.isObject(val)) { if ('index' in val) { f.cell_priority = val['index']; idx = (idx > val['index']) ? idx + 1 : val['index']; } else { i = _.indexOf(tblCols, f.name); f.cell_priority = idx = ((i > idx) ? i : idx); idx = idx + 1; } // We can also provide custom header cell class in schema itself, // But we will give priority to extraClass attached in cols // If headerCell property is already set by cols then skip extraClass property from schema if (!f.headerCell) { if (f.cellHeaderClasses) { f.headerCell = Backgrid.Extension.CustomHeaderCell; } if ('class' in val && _.isString(val['class'])) { f.headerCell = Backgrid.Extension.CustomHeaderCell; f.cellHeaderClasses = (f.cellHeaderClasses || '') + ' ' + val['class']; } } } if (_.isString(val)) { i = _.indexOf(tblCols, f.name); f.cell_priority = idx = ((i > idx) ? i : idx); idx = idx + 1; if (!f.headerCell) { f.headerCell = Backgrid.Extension.CustomHeaderCell; } f.cellHeaderClasses = (f.cellHeaderClasses || '') + ' ' + val; } }; } // Prepare columns for backgrid _.each(groups, function(group) { _.each(group.fields, function(f) { if (!f.cell) { return; } // Check custom property in cols & if it is present then attach it to current cell func(f); if (f.cell_priority != -1) { columns.push(f); } }); schema.push(group); }); return { 'columns': _.sortBy(columns, function(c) { return c.cell_priority; }), 'schema': schema, }; }; Backform.BinaryPathsGridControl = Backform.Control.extend({ initialize: function() { Backform.Control.prototype.initialize.apply(this, arguments); var BinaryPathModel = Backbone.Model.extend({ idAttribute: 'serverType', defaults: { serverType: undefined, binaryPath: undefined, isDefault: false }, }); this.gridColumns = [{ name: 'isDefault', label: gettext('Set as default'), sortable: false, cell: Backgrid.RadioCell, cellHeaderClasses: 'width_percent_10', headerCell: Backgrid.Extension.CustomHeaderCell, deps: ['binaryPath'], editable: function(m) { if (!_.isUndefined(m.get('binaryPath')) && !_.isNull(m.get('binaryPath')) && m.get('binaryPath') !== '') { return true; } else if (!_.isUndefined(m.get('isDefault')) && !_.isNull(m.get('isDefault'))){ setTimeout(function() { m.set('isDefault', false); }, 10); } return false; } }, { name: 'serverType', label: gettext('Database Server'), editable: false, cell: 'string', cellHeaderClasses: 'width_percent_20', headerCell: Backgrid.Extension.CustomHeaderCell, }, { name: 'binaryPath', label: gettext('Binary Path'), sortable: false, cell: Backgrid.Extension.SelectFileCell, dialog_type: 'select_folder', dialog_title: gettext('Select Folder'), placeholder: pgAdmin.server_mode === 'False' ? gettext('Select binary path...') : pgAdmin.enable_binary_path_browsing ? gettext('Select binary path...') : gettext('Enter binary path...'), browse_btn_label: gettext('Select path'), check_btn_label: gettext('Validate utilities'), browse_btn_visible: pgAdmin.server_mode === 'False' ? true : pgAdmin.enable_binary_path_browsing ? true : false }]; var BinPathCollection = this.BinPathCollection = new (Backbone.Collection.extend({ model: BinaryPathModel }))(null); let bin_value = JSON.parse(this.model.get(this.field.get('name'))); this.BinPathCollection.add(bin_value); this.listenTo(BinPathCollection, 'change', this.binPathCollectionChanged); }, render: function() { var self = this, gridHeader = ['
', ' ' + gettext(this.field.get('label')) + '', '
', ].join('\n'), gridBody = $('
').append(gridHeader); self.grid = new Backgrid.Grid({ columns: self.gridColumns, collection: self.BinPathCollection, className: 'backgrid table presentation table-bordered table-noouter-border table-hover', }); this.$el.empty(); this.$el.append(gridBody.append(self.grid.render().$el)); this.$el.append(['' + 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.') + ''].join('\n')); return this; }, binPathCollectionChanged: function() { let bin_value = JSON.stringify(this.BinPathCollection.toJSON()); this.model.set(this.field.get('name'), bin_value, { silent: false }); } }); Backform.UniqueColCollectionControl = Backform.Control.extend({ initialize: function() { Backform.Control.prototype.initialize.apply(this, arguments); var uniqueCol = this.field.get('uniqueCol') || [], m = this.field.get('model'), schema = m.prototype.schema || m.__super__.schema, columns = [], self = this; _.each(schema, function(s) { columns.push(s.id); }); // Check if unique columns provided are also in model attributes. if (uniqueCol.length > _.intersection(columns, uniqueCol).length) { var errorMsg = 'Developer: Unique columns [ ' + _.difference(uniqueCol, columns) + ' ] not found in collection model [ ' + columns + ' ].'; throw errorMsg; } var collection = self.collection = self.model.get(self.field.get('name')); if (!collection) { collection = self.collection = new(pgAdmin.Browser.Node.Collection)( null, { model: self.field.get('model'), silent: true, handler: self.model, top: self.model.top || self.model, attrName: self.field.get('name'), }); self.model.set(self.field.get('name'), collection, { silent: true, }); } if (this.field.get('version_compatible')) { self.listenTo(collection, 'add', self.collectionChanged); self.listenTo(collection, 'change', self.collectionChanged); } }, cleanup: function() { this.stopListening(this.collection, 'change', this.collectionChanged); if (this.field.get('version_compatible')) { this.stopListening(self.collection, 'add', this.collectionChanged); this.stopListening(self.collection, 'change', this.collectionChanged); } if (this.grid) { this.grid.remove(); delete this.grid; } this.$el.empty(); }, collectionChanged: function(newModel, coll, op) { var uniqueCol = this.field.get('uniqueCol') || [], uniqueChangedAttr = [], self = this; // Check if changed model attributes are also in unique columns. And then only check for uniqueness. if (newModel.attributes) { _.each(uniqueCol, function(col) { if (_.has(newModel.attributes, col)) { uniqueChangedAttr.push(col); } }); if (uniqueChangedAttr.length == 0) { return; } } else { return; } var collection = this.model.get(this.field.get('name')); this.stopListening(collection, 'change', this.collectionChanged); // Check if changed attribute's value of new/updated model also exist for another model in collection. // If duplicate value exists then set the attribute's value of new/updated model to its previous values. var m = undefined, oldModel = undefined; collection.each(function(model) { if (newModel != model) { var duplicateAttrValues = []; _.each(uniqueCol, function(attr) { var attrValue = newModel.get(attr); if (!_.isUndefined(attrValue) && attrValue == model.get(attr)) { duplicateAttrValues.push(attrValue); } }); if (duplicateAttrValues.length == uniqueCol.length) { m = newModel; // Keep reference of model to make it visible in dialog. oldModel = model; } } }); if (m) { if (op && op.add) { // Remove duplicate model. setTimeout(function() { collection.remove(m); }, 0); } else { /* * Set model value to its previous value as its new value is * conflicting with another model value. */ m.set(uniqueChangedAttr[0], m.previous(uniqueChangedAttr[0])); } if (oldModel) { var idx = collection.indexOf(oldModel); if (idx > -1) { var newRow = self.grid.body.rows[idx].$el; newRow.addClass('new'); $(newRow).pgMakeVisible('backform-tab'); setTimeout(function() { newRow.removeClass('new'); }, 3000); } } } this.listenTo(collection, 'change', this.collectionChanged); }, render: function() { // Clean up existing elements this.undelegateEvents(); this.$el.empty(); var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), evalF = function(f, d, m) { return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f); }; // Evaluate the disabled, visible, required, canAdd, & canDelete option _.extend(data, { disabled: (field.version_compatible && evalF.apply(this.field, [data.disabled, data, this.model]) ), visible: evalF.apply(this.field, [data.visible, data, this.model]), required: evalF.apply(this.field, [data.required, data, this.model]), canAdd: (field.version_compatible && evalF.apply(this.field, [data.canAdd, data, this.model]) ), canAddRow: data.canAddRow, canDelete: evalF.apply(this.field, [data.canDelete, data, this.model]), canEdit: evalF.apply(this.field, [data.canEdit, data, this.model]), }); _.extend(data, { add_label: '', }); // This control is not visible, we should remove it. if (!data.visible) { return this; } this.control_data = _.clone(data); // Show Backgrid Control var grid = this.showGridControl(data); this.$el.html(grid).addClass(field.name); this.updateInvalid(); this.delegateEvents(); return this; }, showGridControl: function(data) { data.cId = data.cId || _.uniqueId('pgC_'); var self = this, gridHeader = _.template([ '
', ' <%-label%>', ' ', '
', ].join('\n')), gridBody = $('
').append( gridHeader(data) ); // Clean up existing grid if any (in case of re-render) if (self.grid) { self.grid.remove(); } if (!(data.subnode)) { return ''; } var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype, gridSchema = Backform.generateGridColumnsFromModel( data.node_info, subnode, this.field.get('mode'), data.columns ); // Set visibility of Add button if (data.mode == 'properties') { $(gridBody).find('button.add').remove(); } // Insert Delete Cell into Grid if (!data.disabled && data.canDelete) { gridSchema.columns.unshift({ name: 'pg-backform-delete', label: '', cell: Backgrid.Extension.DeleteCell, editable: false, cell_priority: -1, canDeleteRow: data.canDeleteRow, }); } // Insert Edit Cell into Grid if (data.disabled == false && data.canEdit) { var editCell = Backgrid.Extension.ObjectCell.extend({ schema: gridSchema.schema, }); gridSchema.columns.unshift({ name: 'pg-backform-edit', label: '', cell: editCell, cell_priority: -2, canEditRow: data.canEditRow, }); } var collection = this.model.get(data.name); var cellEditing = function(args) { var that = this, cell = args[0]; // Search for any other rows which are open. this.each(function(m) { // Check if row which we are about to close is not current row. if (cell.model != m) { var idx = that.indexOf(m); if (idx > -1) { var row = self.grid.body.rows[idx], rowEditCell = row.$el.find('.subnode-edit-in-process').parent(); // Only close row if it's open. if (rowEditCell.length > 0) { var event = new Event('click'); rowEditCell[0].dispatchEvent(event); } } } }); }; // Listen for any row which is about to enter in edit mode. collection.on('enteringEditMode', cellEditing, collection); // Initialize a new Grid instance self.grid = new Backgrid.Grid({ columns: gridSchema.columns, collection: collection, className: 'backgrid table presentation table-bordered table-noouter-border table-hover', attr: { 'aria-labelledby': data.cId, }, }); for(let i = 0; i < (collection.length); i++) { collection.at(i).parentTr = self.grid.body.rows[i].$el; } // Render subNode grid var subNodeGrid = self.grid.render().$el; // Combine Edit and Delete Cell if (data.canDelete && data.canEdit) { $(subNodeGrid).find('th.pg-backform-delete').remove(); $(subNodeGrid).find('th.pg-backform-edit').attr('colspan', '2'); } var $dialog = gridBody.append(subNodeGrid); let tmp_browser = pgBrowser; if (pgBrowser.preferences_cache.length == 0) tmp_browser = pgWindow.default.pgAdmin.Browser; let preferences = tmp_browser.get_preferences_for_module('browser'); if (preferences) { let addBtn = $dialog.find('.add'); // Add title to the buttons $(addBtn) .attr('title', keyboardShortcuts.shortcut_title(gettext('Add new row'),preferences.add_grid_row)); } // Add button callback if (!(data.disabled || data.canAdd == false)) { $dialog.find('button.add').first().on('click',(e) => { e.preventDefault(); var canAddRow = _.isFunction(data.canAddRow) ? data.canAddRow.apply(self, [self.model]) : true; if (canAddRow) { // Close any existing expanded row before adding new one. _.each(self.grid.body.rows, function(row) { var rowEditCell = row.$el.find('.subnode-edit-in-process').parent(); // Only close row if it's open. if (rowEditCell.length > 0) { var event = new Event('click'); rowEditCell[0].dispatchEvent(event); } }); var allowMultipleEmptyRows = !!self.field.get('allowMultipleEmptyRows'); // If allowMultipleEmptyRows is not set or is false then don't allow second new empty row. // There should be only one empty row. if (!allowMultipleEmptyRows && collection) { var isEmpty = false; collection.each(function(model) { var modelValues = []; _.each(model.attributes, function(val) { modelValues.push(val); }); if (!_.some(modelValues, _.identity)) { isEmpty = true; } }); if (isEmpty) { return false; } } $(self.grid.body.$el.find($('tr.new'))).removeClass('new'); var m = new(data.model)(null, { silent: true, handler: collection, top: self.model.top || self.model, collection: collection, node_info: self.model.node_info, }); if(data.beforeAdd) { m = data.beforeAdd.apply(self, [m]); } collection.add(m); var idx = collection.indexOf(m), newRow = self.grid.body.rows[idx].$el; collection.get(m).parentTr = newRow; m.parentTr = newRow; newRow.addClass('new'); if(!$(newRow).pgMakeBackgridVisible('.backform-tab')){ // We can have subnode controls in Panels $(newRow).pgMakeBackgridVisible('.set-group'); } return false; } }); } return $dialog; }, clearInvalid: function() { this.$el.removeClass('subnode-error'); this.$el.find('.pgadmin-control-error-message').remove(); return this; }, updateInvalid: function() { var self = this, errorModel = self.model.errorModel; if (!(errorModel instanceof Backbone.Model)) return this; this.clearInvalid(); }, }); Backform.SubNodeCollectionControl = Backform.Control.extend({ row: Backgrid.Row, render: function() { var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), evalF = function(f, d, m) { return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f); }; // Evaluate the disabled, visible, required, canAdd, cannEdit & canDelete option _.extend(data, { disabled: evalF(data.disabled, data, this.model), visible: evalF(data.visible, data, this.model), required: evalF(data.required, data, this.model), canAdd: evalF(data.canAdd, data, this.model), canAddRow: data.canAddRow, canEdit: evalF(data.canEdit, data, this.model), canDelete: evalF(data.canDelete, data, this.model), showError: data.showError || true, }); // Show Backgrid Control var grid = (data.subnode == undefined) ? '' : this.showGridControl(data); // Clean up first this.$el.removeClass(Backform.hiddenClassName); if (!data.visible) this.$el.addClass(Backform.hiddenClassName); this.$el.html(grid).addClass(field.name); this.updateInvalid(); return this; }, updateInvalid: function() { var self = this; var errorModel = this.model.errorModel; if (!(errorModel instanceof Backbone.Model)) return this; this.clearInvalid(); var attrArr = self.field.get('name').split('.'), path = attrArr.join('.'), error = self.keyPathAccessor(errorModel.toJSON(), path); if (_.isEmpty(error)) return; if (self.field.get('showError')) { self.$el.addClass('subnode-error').append( $('
').addClass('pgadmin-control-error-message pg-el-offset-4 pg-el-8 help-block').text(error) ); } }, cleanup: function() { // Clean up existing grid if any (in case of re-render) if (this.grid) { this.grid.remove(); } if (this.collection) { this.collection.off('enteringEditMode'); } }, clearInvalid: function() { if (this.field.get('showError')) { this.$el.removeClass('subnode-error'); this.$el.find('.pgadmin-control-error-message').remove(); } return this; }, showGridControl: function(data) { var self = this, gridHeader = ['
', ' ' + data.label + '', ' ', '
', ].join('\n'), gridBody = $('
').append(gridHeader); var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype, gridSchema = Backform.generateGridColumnsFromModel( data.node_info, subnode, this.field.get('mode'), data.columns, data.schema_node ); // Clean up existing grid if any (in case of re-render) if (self.grid) { self.grid.remove(); } // Set visibility of Add button if (data.disabled || data.canAdd == false) { $(gridBody).find('button.add').remove(); } // Insert Delete Cell into Grid if (data.disabled == false && data.canDelete) { gridSchema.columns.unshift({ name: 'pg-backform-delete', label: '', cell: Backgrid.Extension.DeleteCell, editable: false, cell_priority: -1, canDeleteRow: data.canDeleteRow, customDeleteMsg: data.customDeleteMsg, customDeleteTitle: data.customDeleteTitle, }); } // Insert Edit Cell into Grid if (data.disabled == false && data.canEdit) { var editCell = Backgrid.Extension.ObjectCell.extend({ schema: gridSchema.schema, }), canEdit = self.field.has('canEdit') && self.field.get('canEdit') || true; gridSchema.columns.unshift({ name: 'pg-backform-edit', label: '', cell: editCell, cell_priority: -2, editable: canEdit, canEditRow: data.canEditRow, }); } var collection = self.model.get(data.name); if (!collection) { collection = new(pgBrowser.Node.Collection)(null, { handler: self.model.handler || self.model, model: data.model, top: self.model.top || self.model, silent: true, }); self.model.set(data.name, collection, { silent: true, }); } var cellEditing = function(args) { var ctx = this, cell = args[0]; // Search for any other rows which are open. this.each(function(m) { // Check if row which we are about to close is not current row. if (cell.model != m) { var idx = ctx.indexOf(m); if (idx > -1) { var row = grid.body.rows[idx], rowEditCell = row.$el.find('.subnode-edit-in-process').parent(); // Only close row if it's open. if (rowEditCell.length > 0) { var event = new Event('click'); rowEditCell[0].dispatchEvent(event); } } } }); }; // Listen for any row which is about to enter in edit mode. collection.on('enteringEditMode', cellEditing, collection); // Initialize a new Grid instance var grid = self.grid = new Backgrid.Grid({ columns: gridSchema.columns, collection: collection, row: this.row, className: 'backgrid table presentation table-bordered table-noouter-border table-hover', }); // Render subNode grid var subNodeGrid = grid.render().$el; // Combine Edit and Delete Cell if (data.canDelete && data.canEdit) { $(subNodeGrid).find('th.pg-backform-delete').remove(); $(subNodeGrid).find('th.pg-backform-edit').attr('colspan', '2'); } var $dialog = gridBody.append(subNodeGrid); let tmp_browser = pgBrowser; if (pgBrowser.preferences_cache.length == 0) tmp_browser = pgWindow.default.pgAdmin.Browser; let preferences = tmp_browser.get_preferences_for_module('browser'); if (preferences) { let addBtn = $dialog.find('.add'); // Add title to the buttons $(addBtn) .attr('title', keyboardShortcuts.shortcut_title(gettext('Add new row'),preferences.add_grid_row)); } // Add button callback $dialog.find('button.add').on('click',(e) => { e.preventDefault(); var canAddRow = _.isFunction(data.canAddRow) ? data.canAddRow.apply(self, [self.model]) : true; if (canAddRow) { // Close any existing expanded row before adding new one. _.each(grid.body.rows, function(row) { var rowEditCell = row.$el.find('.subnode-edit-in-process').parent(); // Only close row if it's open. if (rowEditCell.length > 0) { var event = new Event('click'); rowEditCell[0].dispatchEvent(event); } }); grid.insertRow({}); var newRow = $(grid.body.rows[collection.length - 1].$el); newRow.attr('class', 'new').on('click',() => { $(this).attr('class', 'editable'); }); if(!$(newRow).pgMakeBackgridVisible('.backform-tab')){ // We can have subnode controls in Panels $(newRow).pgMakeBackgridVisible('.set-group'); } return false; } }); return $dialog; }, }); /* * SQL Tab Control for showing the modified SQL for the node with the * property 'hasSQL' is set to true. * * When the user clicks on the SQL tab, we will send the modified data to the * server and fetch the SQL for it. */ Backform.SqlTabControl = Backform.Control.extend({ defaults: { label: '', controlsClassName: 'pgadmin-controls pg-el-sm-12 SQL', extraClasses: [], helpMessage: null, }, template: _.template([ '', '
', ' ', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), /* * Initialize the SQL Tab control properly */ initialize: function(o) { Backform.Control.prototype.initialize.apply(this, arguments); // Save the required information for using it later. this.dialog = o.dialog; this.tabIndex = o.tabIndex; _.bindAll(this, 'onTabChange', 'onPanelResized'); }, getValueFromDOM: function() { return this.formatter.toRaw(this.$el.find('textarea').val(), this.model); }, reflectPreferences: function() { var self = this; /* self.sqlCtrl is null when SQL tab is not active */ if(self.sqlCtrl) { let sqlEditPreferences = pgAdmin.Browser.get_preferences_for_module('sqleditor'); $(self.sqlCtrl.getWrapperElement()).css( 'font-size',SqlEditorUtils.calcFontSize(sqlEditPreferences.sql_font_size) ); self.sqlCtrl.setOption('tabSize', sqlEditPreferences.tab_size); self.sqlCtrl.setOption('lineWrapping', sqlEditPreferences.wrap_code); self.sqlCtrl.setOption('autoCloseBrackets', sqlEditPreferences.insert_pair_brackets); self.sqlCtrl.setOption('matchBrackets', sqlEditPreferences.brace_matching); self.sqlCtrl.refresh(); } }, render: function() { if (this.sqlCtrl) { this.sqlCtrl.toTextArea(); delete this.sqlCtrl; this.sqlCtrl = null; this.$el.empty(); this.model.off('pg-property-tab-changed', this.onTabChange, this); this.model.off('pg-browser-resized', this.onPanelResized, this); } // Use the Backform Control's render function Backform.Control.prototype.render.apply(this, arguments); this.sqlCtrl = CodeMirror.fromTextArea( (this.$el.find('textarea')[0]), { lineNumbers: true, mode: 'text/x-pgsql', readOnly: true, extraKeys: pgAdmin.Browser.editor_shortcut_keys, screenReaderLabel: 'SQL', }); this.reflectPreferences(); /* Check for sql editor preference changes */ let self = this; this.$el.find('textarea').attr('tabindex', -1); pgBrowser.onPreferencesChange('sqleditor', function() { self.reflectPreferences(); }); /* * We will listen to the tab change event to check, if the SQL tab has * been clicked or, not. */ this.model.on('pg-property-tab-changed', this.onTabChange, this); this.model.on('pg-browser-resized', this.onPanelResized, this); return this; }, onTabChange: function(obj) { // Fetch the information only if the SQL tab is visible at the moment. if (this.dialog && obj.shown == this.tabIndex) { // We will send a request to the sever only if something has changed // in a model and also it does not contain any error. if (this.model.sessChanged()) { if (_.size(this.model.errorModel.attributes) == 0) { var self = this, node = self.field.get('schema_node'), msql_url = node.generate_url.apply( node, [ null, 'msql', this.field.get('node_data'), !self.model.isNew(), this.field.get('node_info'), node.url_jump_after_node, ]); // Fetching the modified SQL self.model.trigger('pgadmin-view:msql:fetching', self.method, node); $.ajax({ url: msql_url, type: 'GET', cache: false, data: self.model.toJSON(true, 'GET'), dataType: 'json', contentType: 'application/json', }).done(function(res) { self.sqlCtrl.clearHistory(); self.sqlCtrl.setValue(res.data); }).fail(function() { self.model.trigger('pgadmin-view:msql:error', self.method, node, arguments); }).always(function() { self.model.trigger('pgadmin-view:msql:fetched', self.method, node, arguments); }); } else { this.sqlCtrl.clearHistory(); this.sqlCtrl.setValue('-- ' + gettext('Definition incomplete.')); } } else { this.sqlCtrl.clearHistory(); this.sqlCtrl.setValue('-- ' + gettext('No updates.')); } this.sqlCtrl.refresh.apply(this.sqlCtrl); } }, onPanelResized: function(o) { if (o && o.container) { var $tabContent = o.container.find( '.backform-tab > .tab-content' ).first(), $sqlPane = $tabContent.find( 'div[role=tabpanel].tab-pane.SQL' ); if ($sqlPane.hasClass('active')) { $sqlPane.find('.CodeMirror').css( 'cssText', 'height: ' + ($tabContent.height() + 8) + 'px !important;' ); } } }, remove: function() { if (this.sqlCtrl) { this.sqlCtrl.toTextArea(); delete this.sqlCtrl; this.sqlCtrl = null; this.$el.empty(); } this.model.off('pg-property-tab-changed', this.onTabChange, this); this.model.off('pg-browser-resized', this.onPanelResized, this); Backform.Control.__super__.remove.apply(this, arguments); }, }); /* * Numeric input Control functionality just like backgrid */ Backform.NumericControl = Backform.InputControl.extend({ defaults: { type: 'number', label: '', min: undefined, max: undefined, maxlength: 255, extraClasses: [], helpMessage: null, }, template: _.template([ '', '
', ' <%=readonly ? "readonly aria-readonly=true" : ""%> <%=required ? "required" : ""%> />', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', '
', ].join('\n')), }); /////// // Generate a schema (as group members) based on the model's schema // // It will be used by the grid, properties, and dialog view generation // functions. Backform.generateViewSchema = function( node_info, Model, mode, node, treeData, noSQL, subschema ) { var proto = (Model && Model.prototype) || Model, schema = subschema || (proto && proto.schema), fields = [], groupInfo = {}; // 'schema' has the information about how to generate the form. if (schema && _.isArray(schema)) { var evalASFunc = evalASFunc = function(prop) { return ((prop && proto[prop] && typeof proto[prop] == 'function') ? proto[prop] : prop); }; var groups = {}, server_info = node_info && ('server' in node_info) && pgBrowser.serverInfo && pgBrowser.serverInfo[node_info.server._id], in_catalog = node_info && ('catalog' in node_info), ver_in_limit; _.each(schema, function(s) { // Do we understand - what control, we're creating // here? if (s.type == 'group') { var visible = true; ver_in_limit = (_.isUndefined(server_info) ? true : ((_.isUndefined(s.server_type) ? true : (server_info.type in s.server_type)) && (_.isUndefined(s.min_version) ? true : (server_info.version >= s.min_version)) && (_.isUndefined(s.max_version) ? true : (server_info.version <= s.max_version)))); if (s.mode && _.isObject(s.mode)) visible = (_.indexOf(s.mode, mode) > -1); if (visible) visible = evalASFunc(s.visible); groupInfo[s.id] = { label: s.label || s.id, version_compatible: ver_in_limit, visible: visible, tabPanelCodeClass: '', }; return; } if (!s.mode || (s && s.mode && _.isObject(s.mode) && _.indexOf(s.mode, mode) != -1)) { // Each field is kept in specified group, or in // 'General' category. var group = s.group || gettext('General'), control = s.control || Backform.getMappedControl(s.type, mode), cell = s.cell || Backform.getMappedControl(s.type, 'cell'); if (control == null) { return; } // Generate the empty group list (if not exists) groups[group] = (groups[group] || []); ver_in_limit = (_.isUndefined(server_info) ? true : ((_.isUndefined(s.server_type) ? true : (server_info.type in s.server_type)) && (_.isUndefined(s.min_version) ? true : (server_info.version >= s.min_version)) && (_.isUndefined(s.max_version) ? true : (server_info.version <= s.max_version)))); var disabled = ( !ver_in_limit || in_catalog ), readonly = (mode == 'properties'), schema_node = (s.node && _.isString(s.node) && s.node in pgBrowser.Nodes && pgBrowser.Nodes[s.node]) || node; var o = _.extend(_.clone(s), { name: s.id, // This can be disabled in some cases (if not hidden) disabled: (disabled ? true : evalASFunc(s.disabled)), readonly: (readonly ? true : evalASFunc(s.readonly)), editable: _.isUndefined(s.editable) ? pgAdmin.editableCell : evalASFunc(s.editable), subnode: ((_.isString(s.model) && s.model in pgBrowser.Nodes) ? pgBrowser.Nodes[s.model].model : s.model), canAdd: (disabled || readonly) ? false : evalASFunc(s.canAdd), canAddRow: (disabled || readonly) ? false : evalASFunc(s.canAddRow), canEdit: (disabled || readonly) ? false : evalASFunc(s.canEdit), canDelete: (disabled || readonly) ? false : evalASFunc(s.canDelete), canEditRow: (disabled || readonly) ? false : evalASFunc(s.canEditRow), canDeleteRow: (disabled || readonly) ? false : evalASFunc(s.canDeleteRow), transform: evalASFunc(s.transform), mode: mode, control: control, cell: cell, node_info: node_info, schema_node: schema_node, // Do we need to show this control in this mode? visible: evalASFunc(s.visible), node: node, node_data: treeData, version_compatible: ver_in_limit, }); delete o.id; // Temporarily store in dictionary format for // utilizing it later. groups[group].push(o); if (s.type == 'nested') { delete o.name; delete o.cell; o.schema = Backform.generateViewSchema( node_info, Model, mode, node, treeData, true, s.schema ); o.control = o.control || 'tab'; } } }); // Do we have fields to genreate controls, which we // understand? if (_.isEmpty(groups)) { return null; } if (!noSQL && node && node.hasSQL && (mode == 'create' || mode == 'edit')) { groups[gettext('SQL')] = [{ name: 'sql', visible: true, disabled: false, type: 'text', control: 'sql-tab', node_info: node_info, schema_node: node, node_data: treeData, }]; } // Create an array from the dictionary with proper required // structure. _.each(groups, function(val, key) { let tabPanelCodeClass = _.pluck(val, 'tabPanelCodeClass'); if (tabPanelCodeClass) { tabPanelCodeClass = tabPanelCodeClass.join(' ').trim(); } fields.push( _.extend( _.defaults( groupInfo[key] || { label: key, tabPanelCodeClass: tabPanelCodeClass, }, { version_compatible: true, visible: true, } ), { fields: val, }) ); }); } return fields; }; var Select2Formatter = function() {}; _.extend(Select2Formatter.prototype, { fromRaw: function(rawData) { return encodeURIComponent(rawData); }, toRaw: function(formattedData, model, opts) { if (_.isArray(formattedData)) { if (opts && opts.tags) return formattedData; return _.map(formattedData, decodeURIComponent); } else { if (!_.isNull(formattedData) && !_.isUndefined(formattedData)) { return decodeURIComponent(formattedData); } else { return null; } } }, }); /* * Backform Select2 control. */ Backform.Select2Control = Backform.SelectControl.extend({ defaults: _.extend({}, Backform.SelectControl.prototype.defaults, { select2: { first_empty: true, multiple: false, emptyOptions: false, preserveSelectionOrder: false, isDropdownParent: false, }, // To accept the label and conrol classes while extending control if // required(e.g. if we want to show label and control in 50-50% or in //different width of dialog/form) otherwise default classes will be added controlLabelClassName: Backform.controlLabelClassName, controlsClassName: Backform.controlsClassName, }), events: function() { // Inherit all default events of InputControl return _.extend({}, Backform.SelectControl.prototype.events, { 'select2:select': 'onSelect', }); }, onSelect: function (evt) { var sel2Options = this.field.get('select2'); if (!_.isUndefined(sel2Options) && !_.isNull(sel2Options) && sel2Options.multiple && sel2Options.preserveSelectionOrder) { var element = evt.params.data.element; var $element = $(element); $element.detach(); $(this.$sel).append($element); $(this.$sel).trigger('change'); } let new_value = _.findWhere(this.field.get('options'), {value: evt.params.data.id}); if(new_value && !_.isUndefined(new_value.preview_src) && new_value.preview_src) { this.$el.find('.preview-img img').attr('src', new_value.preview_src); } }, formatter: Select2Formatter, template: _.template([ '<% if(label == false) {} else {%>', ' ', '<% }%>', '
', ' ', ' <% if (helpMessage && helpMessage.length) { %>', ' <%=helpMessage%>', ' <% } %>', ' <% for (var i=0; i < options.length; i++) {%>', ' <% var option = options[i]; %>', ' <% if (option.preview_src && option.value === rawValue) { %>', '
', ' '+gettext('Preview not available...')+'', '
', ' <%}%>', ' <%}%>', '
', ].join('\n')), render: function() { if (this.$sel && this.$sel.select2 && this.$sel.select2.hasOwnProperty('destroy')) { this.$sel.select2('destroy'); } var field = _.defaults(this.field.toJSON(), this.defaults), attributes = this.model.toJSON(), attrArr = field.name.split('.'), name = attrArr.shift(), path = attrArr.join('.'), rawValue = this.keyPathAccessor(attributes[name], path), data = _.extend(field, { rawValue: rawValue, value: this.formatter.fromRaw(rawValue, this.model), attributes: attributes, formatter: this.formatter, }), evalF = function(f, d, m) { return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f); }; data.select2 = data.select2 || {}; _.defaults(data.select2, this.defaults.select2, { first_empty: true, multiple: false, emptyOptions: false, preserveSelectionOrder: false, isDropdownParent: false, showOnScroll: true, }); // Evaluate the disabled, visible, and required option // disable for readonly also and later handle readonly programmatically. _.extend(data, { disabled: evalF(data.disabled, data, this.model), readonly: evalF(data.readonly, data, this.model), visible: evalF(data.visible, data, this.model), required: evalF(data.required, data, this.model), }); // Evaluation the options if (_.isFunction(data.options)) { try { data.options = data.options(this); } catch (e) { // Do nothing data.options = []; this.model.trigger( 'pgadmin-view:transform:error', this.model, this.field, e ); } } data.cId = data.cId || _.uniqueId('pgC_'); // Clean up first this.$el.removeClass(Backform.hiddenClassName); if (!data.visible) this.$el.addClass(Backform.hiddenClassName); this.$el.html(this.template(data)).addClass(field.name); var select2Opts = _.extend({ disabled: data.disabled || data.readonly, }, field.select2, { options: (this.field.get('options') || this.defaults.options), }); // Dropdown body can be render at user given location // If isDropdownParent flag is set to true then, By default we will // display it on the control itself. if (data.select2.isDropdownParent) { select2Opts.dropdownParent = data.select2.dropdownParent || this.$el; } // If disabled then no need to show placeholder if (data.disabled || data.readonly) { select2Opts['placeholder'] = ''; } /* * Add empty option as Select2 requires any empty '