///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // // Copyright (C) 2013 - 2019, The pgAdmin Development Team // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// define([ 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify', 'moment', 'bignumber', 'bootstrap.datetimepicker', 'bootstrap.switch', 'backgrid.filter', ], function( gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber ) { /* * Add mechanism in backgrid to render different types of cells in * same column; */ // Add new property cellFunction in Backgrid.Column. _.extend(Backgrid.Column.prototype.defaults, { cellFunction: undefined, }); // Add tooltip to cell if cell content is larger than // cell width _.extend(Backgrid.Cell.prototype.events, { 'mouseover': function() { var $el = $(this.el); if ($el.text().length > 0 && !$el.attr('title') && ($el.innerWidth() + 1) < $el[0].scrollWidth ) { $el.attr('title', $.trim($el.text())); } }, }); /* Overriding backgrid sort method. * As we are getting numeric, integer values as string * from server side, but on client side javascript truncates * large numbers automatically due to which backgrid was unable * to sort numeric values properly in the grid. * To fix this issue, now we check if cell type is integer/number * convert it into BigNumber object and make comparison to perform sorting. */ _.extend(Backgrid.Body.prototype, { sort: function(column, direction) { if (!_.contains(['ascending', 'descending', null], direction)) { throw new RangeError('direction must be one of "ascending", "descending" or `null`'); } if (_.isString(column)) column = this.columns.findWhere({ name: column, }); var collection = this.collection; var order; if (direction === 'ascending') order = -1; else if (direction === 'descending') order = 1; else order = null; // Get column type and pass it to comparator. var col_type = column.get('cell').prototype.className || 'string-cell', comparator = this.makeComparator(column.get('name'), order, order ? column.sortValue() : function(model) { return model.cid.replace('c', '') * 1; }, col_type); if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) { collection.setSorting(order && column.get('name'), order, { sortValue: column.sortValue(), }); if (collection.fullCollection) { // If order is null, pageable will remove the comparator on both sides, // in this case the default insertion order comparator needs to be // attached to get back to the order before sorting. if (collection.fullCollection.comparator == null) { collection.fullCollection.comparator = comparator; } collection.fullCollection.sort(); collection.trigger('backgrid:sorted', column, direction, collection); } else collection.fetch({ reset: true, success: function() { collection.trigger('backgrid:sorted', column, direction, collection); }, }); } else { collection.comparator = comparator; collection.sort(); collection.trigger('backgrid:sorted', column, direction, collection); } column.set('direction', direction); return this; }, makeComparator: function(attr, order, func, type) { return function(left, right) { // extract the values from the models var l = func(left, attr), r = func(right, attr), t; if (_.isUndefined(l) || _.isUndefined(r)) return; var types = ['number-cell', 'integer-cell']; if (_.include(types, type)) { var _l, _r; // NaN if invalid number try { _l = new BigNumber(l); } catch (err) { _l = NaN; } try { _r = new BigNumber(r); } catch (err) { _r = NaN; } // if descending order, swap left and right if (order === 1) t = _l, _l = _r, _r = t; if (_l.eq(_r)) // If both are equals return 0; else if (_l.lt(_r)) // If left is less than right return -1; else return 1; } else { // if descending order, swap left and right if (order === 1) t = l, l = r, r = t; // compare as usual if (l === r) return 0; else if (l < r) return -1; return 1; } }; }, }); _.extend(Backgrid.Row.prototype, { makeCell: function(column) { return new(this.getCell(column))({ column: column, model: this.model, }); }, /* * getCell function will check and execute user given cellFunction to get * appropriate cell class for current cell being rendered. * User provided cellFunction must return valid cell class. * cellFunction will be called with context (this) as column and model as * argument. */ getCell: function(column) { var cf = column.get('cellFunction'); if (_.isFunction(cf)) { var cell = cf.apply(column, [this.model]); try { return Backgrid.resolveNameToClass(cell, 'Cell'); } catch (e) { if (e instanceof ReferenceError) { // Fallback to column cell. return column.get('cell'); } else { throw e; // Let other exceptions bubble up } } } else { return column.get('cell'); } }, }); var ObjectCellEditor = Backgrid.Extension.ObjectCellEditor = Backgrid.CellEditor.extend({ modalTemplate: _.template([ '
', '
', '
', ].join('\n')), stringTemplate: _.template([ '
', ' ', '
', ' ', '
', '
', ].join('\n')), extendWithOptions: function(options) { _.extend(this, options); }, render: function() { return this; }, postRender: function(model, column) { var columns_length = this.columns_length, // To render schema directly from Backgrid cell we use columns schema // attribute. schema = this.schema.length ? this.schema : this.column.get('schema'); if (column != null && column.get('name') != this.column.get('name')) return false; if (!_.isArray(schema)) throw new TypeError('schema must be an array'); // Create a Backbone model from our object if it does not exist var $dialog = this.createDialog(columns_length); // Add the Bootstrap form var $form = $('
'); $dialog.find('div.subnode-body').append($form); // Call Backform to prepare dialog var back_el = $dialog.find('form.form-dialog'); this.objectView = new Backform.Dialog({ el: back_el, model: this.model, schema: schema, tabPanelClassName: function() { return 'sub-node-form col-sm-12'; }, }); this.objectView.render(); return this; }, createDialog: function(noofcol) { noofcol = noofcol || 1; var $dialog = this.$dialog = $(this.modalTemplate({ title: '', })), tr = $(''), td = $('', { class: 'editable sortable renderable', style: 'height: auto', colspan: noofcol + 2, }).appendTo(tr); this.tr = tr; // Show the Bootstrap modal dialog td.append($dialog.css('display', 'block')); this.el.parent('tr').after(tr); return $dialog; }, save: function() { // Retrieve values from the form, and store inside the object model this.model.trigger('backgrid:edited', this.model, this.column, new Backgrid.Command({ keyCode: 13, })); if (this.tr) { this.tr.remove(); } return this; }, remove: function() { this.objectView.remove(); Backgrid.CellEditor.prototype.remove.apply(this, arguments); if (this.tr) { this.tr.remove(); } return this; }, }); Backgrid.Extension.PGSelectCell = Backgrid.SelectCell.extend({ // It's possible to render an option group or use a // function to provide option values too. optionValues: function() { var res = [], opts = _.result(this.column.attributes, 'options'); _.each(opts, function(o) { res.push([o.label, o.value]); }); return res; }, }); Backgrid.Extension.ObjectCell = Backgrid.Cell.extend({ editorOptionDefaults: { schema: [], }, className: 'edit-cell', editor: ObjectCellEditor, initialize: function(options) { Backgrid.Cell.prototype.initialize.apply(this, arguments); // Pass on cell options to the editor var cell = this, editorOptions = {}; _.each(this.editorOptionDefaults, function(def, opt) { if (!cell[opt]) cell[opt] = def; if (options && options[opt]) cell[opt] = options[opt]; editorOptions[opt] = cell[opt]; }); editorOptions['el'] = $(this.el); editorOptions['columns_length'] = this.column.collection.length; editorOptions['el'].attr('tabindex', 1); this.listenTo(this.model, 'backgrid:edit', function(model, column, cell, editor) { if (column.get('name') == this.column.get('name')) editor.extendWithOptions(editorOptions); }); }, enterEditMode: function() { // Notify that we are about to enter in edit mode for current cell. // We will check if this row is editable first var canEditRow = (!_.isUndefined(this.column.get('canEditRow')) && _.isFunction(this.column.get('canEditRow'))) ? Backgrid.callByNeed(this.column.get('canEditRow'), this.column, this.model) : true; if (canEditRow) { // Notify that we are about to enter in edit mode for current cell. this.model.trigger('enteringEditMode', [this]); Backgrid.Cell.prototype.enterEditMode.apply(this, arguments); /* Make sure - we listen to the click event */ this.delegateEvents(); var editable = Backgrid.callByNeed(this.column.editable(), this.column, this.model); if (editable) { this.$el.html( '' ); this.model.trigger( 'pg-sub-node:opened', this.model, this ); } } else { Alertify.alert(gettext('This object is not user editable.'), function() { return true; }); } }, render: function() { this.$el.empty(); this.$el.html(''); this.delegateEvents(); if (this.grabFocus) this.$el.trigger('focus'); return this; }, exitEditMode: function() { var index = $(this.currentEditor.objectView.el) .find('.nav-tabs > .active > a[data-toggle="tab"]').first() .data('tabIndex'); Backgrid.Cell.prototype.exitEditMode.apply(this, arguments); this.model.trigger( 'pg-sub-node:closed', this, index ); this.grabFocus = true; }, events: { 'click': function(e) { if (this.$el.find('i').first().hasClass('subnode-edit-in-process')) { // Need to redundantly undelegate events for Firefox this.undelegateEvents(); this.currentEditor.save(); } else { this.enterEditMode.call(this, []); } e.preventDefault(); }, }, }); Backgrid.Extension.DeleteCell = Backgrid.Cell.extend({ defaults: _.defaults({ defaultDeleteMsg: gettext('Are you sure you wish to delete this row?'), defaultDeleteTitle: gettext('Delete Row'), }, Backgrid.Cell.prototype.defaults), /** @property */ className: 'delete-cell', events: { 'click': 'deleteRow', }, deleteRow: function(e) { e.preventDefault(); var that = this; // We will check if row is deletable or not var canDeleteRow = (!_.isUndefined(this.column.get('canDeleteRow')) && _.isFunction(this.column.get('canDeleteRow'))) ? Backgrid.callByNeed(this.column.get('canDeleteRow'), this.column, this.model) : true; if (canDeleteRow) { var delete_msg = !_.isUndefined(this.column.get('customDeleteMsg')) ? this.column.get('customDeleteMsg') : that.defaults.defaultDeleteMsg; var delete_title = !_.isUndefined(this.column.get('customDeleteTitle')) ? this.column.get('customDeleteTitle') : that.defaults.defaultDeleteTitle; Alertify.confirm( delete_title, delete_msg, function() { that.model.collection.remove(that.model); }, function() { return true; } ); } else { Alertify.alert(gettext('This object cannot be deleted.'), function() { return true; } ); } }, initialize: function() { Backgrid.Cell.prototype.initialize.apply(this, arguments); }, render: function() { this.$el.empty(); this.$el.html(''); this.delegateEvents(); return this; }, }); Backgrid.Extension.CustomHeaderCell = Backgrid.HeaderCell.extend({ initialize: function() { // Here, we will add custom classes to header cell Backgrid.HeaderCell.prototype.initialize.apply(this, arguments); var getClassName = this.column.get('cellHeaderClasses'); if (getClassName) { this.$el.addClass(getClassName); } }, }); /** SwitchCell renders a Bootstrap Switch in backgrid cell */ if (window.jQuery && window.jQuery.fn.bootstrapSwitch) $.fn.bootstrapSwitch = window.jQuery.fn.bootstrapSwitch; Backgrid.Extension.SwitchCell = Backgrid.BooleanCell.extend({ defaults: { options: _.defaults({ onText: gettext('True'), offText: gettext('False'), onColor: 'success', offColor: 'default', size: 'mini', }, $.fn.bootstrapSwitch.defaults), }, className: 'switch-cell', initialize: function() { Backgrid.BooleanCell.prototype.initialize.apply(this, arguments); this.onChange = this.onChange.bind(this); }, enterEditMode: function() { this.$el.addClass('editor'); $(this.$el.find('input')).trigger('focus'); }, exitEditMode: function() { this.$el.removeClass('editor'); }, events: { 'switchChange.bootstrapSwitch': 'onChange', }, onChange: function() { var model = this.model, column = this.column, val = this.formatter.toRaw(this.$input.prop('checked'), model); this.enterEditMode(); // on bootstrap change we also need to change model's value model.set(column.get('name'), val); }, render: function() { var self = this, col = _.defaults(this.column.toJSON(), this.defaults), model = this.model, column = this.column, rawValue = this.formatter.fromRaw( model.get(column.get('name')), model ), editable = Backgrid.callByNeed(col.editable, column, model); this.undelegateEvents(); this.$el.empty(); this.$el.append( $('', { tabIndex: -1, type: 'checkbox', }).prop('checked', rawValue).prop('disabled', !editable)); this.$input = this.$el.find('input[type=checkbox]').first(); // Override BooleanCell checkbox with Bootstrapswitch this.$input.bootstrapSwitch( _.defaults({ 'state': rawValue, 'disabled': !editable, }, col.options, this.defaults.options )); // Listen for Tab key this.$el.on('keydown', function(e) { var gotoCell; if (e.keyCode == 9) { // go to Next Cell & if Shift is also pressed go to Previous Cell gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next(); } if (gotoCell) { setTimeout(function() { if (gotoCell.hasClass('editable')) { e.preventDefault(); e.stopPropagation(); var command = new Backgrid.Command({ key: 'Tab', keyCode: 9, which: 9, shiftKey: e.shiftKey, }); self.model.trigger('backgrid:edited', self.model, self.column, command); gotoCell.trigger('focus'); } }, 20); } }); this.delegateEvents(); return this; }, }); /* * Select2Cell for backgrid. */ Backgrid.Extension.Select2Cell = Backgrid.SelectCell.extend({ className: 'select2-cell', /** @property */ editor: null, defaults: _.defaults({ select2: {}, opt: { label: null, value: null, selected: false, }, }, Backgrid.SelectCell.prototype.defaults), enterEditMode: function() { if (!this.$el.hasClass('editor')) this.$el.addClass('editor'); this.$select.select2('focus'); this.$select.select2('open'); this.$select.on('blur', this.exitEditMode); }, exitEditMode: function() { this.$select.off('blur', this.exitEditMode); this.$select.select2('close'); this.$el.removeClass('editor'); }, events: { 'select2:open': 'enterEditMode', 'select2:close': 'exitEditMode', 'change': 'onSave', 'select2:unselect': 'onSave', }, /** @property {function(Object, ?Object=): string} template */ template: _.template([ '', ].join(''), null, { variable: null, }), initialize: function() { Backgrid.SelectCell.prototype.initialize.apply(this, arguments); this.onSave = this.onSave.bind(this); this.enterEditMode = this.enterEditMode.bind(this); this.exitEditMode = this.exitEditMode.bind(this); }, render: function() { var col = _.defaults(this.column.toJSON(), this.defaults), model = this.model, column = this.column, editable = Backgrid.callByNeed(col.editable, column, model), optionValues = _.clone(this.optionValues || (_.isFunction(this.column.get('options')) ? (this.column.get('options'))(this) : this.column.get('options'))); this.undelegateEvents(); if (this.$select) { if (this.$select.data('select2')) { this.$select.select2('destroy'); } delete this.$select; this.$select = null; } this.$el.empty(); if (!_.isArray(optionValues)) throw new TypeError('optionValues must be an array'); /* * Add empty option as Select2 requires any empty '