diff --git a/web/pgadmin/browser/static/js/menu.js b/web/pgadmin/browser/static/js/menu.js index e2f99ec69..26a48316e 100644 --- a/web/pgadmin/browser/static/js/menu.js +++ b/web/pgadmin/browser/static/js/menu.js @@ -4,7 +4,9 @@ function(_, pgAdmin, $) { 'use strict'; pgAdmin.Browser = pgAdmin.Browser || {}; - pgAdmin.Browser.MenuItem = function(opts) { + + // Individual menu-item class + var MenuItem = pgAdmin.Browser.MenuItem = function(opts) { var menu_opts = [ 'name', 'label', 'priority', 'module', 'callback', 'data', 'enable', 'category', 'target', 'url', 'icon' @@ -18,7 +20,32 @@ function(_, pgAdmin, $) { }; _.extend(pgAdmin.Browser.MenuItem.prototype, { + /* + * Keeps track of the jQuery object representing this menu-item. This will + * be used by the update function to enable/disable the individual item. + */ + $el: null, + /* + * Generate the UI for this menu-item. enable/disable, based on the + * currently selected object. + */ generate: function(node, item) { + this.create_el(node, item); + + this.context = { + name: this.label, + icon: this.icon || this.module && (this.module.type), + disabled: this.is_disabled, + callback: this.context_menu_callback.bind(this, item) + }; + + return this.$el; + }, + + /* + * Create the jquery element for the menu-item. + */ + create_el: function(node, item) { var url = $('', { 'id': this.name, 'href': this.url, @@ -29,15 +56,68 @@ function(_, pgAdmin, $) { cb: this.callback, data: this.data }).addClass('menu-link'); + if (this.icon) { url.append($('', {'class': this.icon})); } url.append($('').text(' ' + this.label)); - return $('
  • ') - .addClass('menu-item' + (this.disabled(node, item) ? ' disabled' : '')) + this.is_disabled = this.disabled(node, item); + this.$el = $('
  • ') + .addClass('menu-item' + (this.is_disabled ? ' disabled' : '')) .append(url); }, + + /* + * Updates the enable/disable state of the menu-item based on the current + * selection using the disabled function. This also creates a object + * for this menu, which can be used in the context-menu. + */ + update: function(node, item) { + + if (!this.$el.hasClass('disabled')) { + this.$el.addClass('disabled'); + } + + this.is_disabled = this.disabled(node, item); + if (!this.is_disabled) { + this.$el.removeClass('disabled'); + } + + this.context = { + name: this.label, + icon: this.icon || (this.module && this.module.type), + disabled: this.is_disabled, + callback: this.context_menu_callback.bind(this, item) + }; + }, + + /* + * This will be called when context-menu is clicked. + */ + context_menu_callback: function(item) { + var o = this, cb; + + if (o.module['callbacks'] && ( + o.callback in o.module['callbacks'] + )) { + cb = o.module['callbacks'][o.callback]; + } else if (o.callback in o.module) { + cb = o.module[o.callback]; + } + if (cb) { + cb.apply(o.module, [o.data, item]); + } else { + pgAdmin.Browser.report_error( + S('Developer Warning: Callback - "%s" not found!'). + sprintf(o.cb).value() + ); + } + }, + + /* + * Checks this menu enable/disable state based on the selection. + */ disabled: function(node, item) { if (this.enable == undefined) return false; @@ -51,6 +131,179 @@ function(_, pgAdmin, $) { } }); + /* + * This a class for creating a menu group, mainly used by the submenu + * creation logic. + * + * Arguments: + * 1. Options to render the submenu DOM Element. + * i.e. label, icon, above (separator), below (separator) + * 2. List of menu-items comes under this submenu. + * 3. Did we rendered separator after the menu-item/submenu? + * 4. A unique-id for this menu-item. + * + * Returns a object, similar to the menu-item, which has his own jQuery + * Element, context menu representing object, etc. + * + */ + + pgAdmin.Browser.MenuGroup = function(opts, items, prev, ctx) { + var template = _.template([ + '<% if (above) { %>
    <% } %>', + '
  • ', + '<% if (below) { %>
    <% } %>',].join('\n')), + data = { + 'label': opts.label, + 'icon': opts.icon, + 'above': opts.above && !prev, + 'below': opts.below, + }, m, + $el = $(template(data)), + $menu = $el.find('.dropdown-menu'), + submenus = {}, + ctxId = 1; + + ctx = _.uniqueId(ctx + '_sub_'); + + // Sort by alphanumeric ordered first + items.sort(function(a, b) {return a.label.localeCompare(b.label);}); + // Sort by priority + items.sort(function(a, b) {return a.priority - b.priority;}); + + for (var idx in items) { + m = items[idx]; + $menu.append(m.$el); + if (!m.is_disabled) { + submenus[ctx + ctxId] = m.context + } + ctxId++; + } + + var is_disabled = (_.size(submenus) == 0); + + return { + $el: $el, + priority: opts.priority || 10, + label: opts.label, + above: data['above'], + below: opts.below, + is_disabled: is_disabled, + context: { + name: opts.label, + icon: opts.icon, + items: submenus, + disabled: is_disabled + } + }; + }; + + /* + * A function to generate menus (submenus) based on the categories. + * Attach the current selected browser tree node to each of the generated + * menu-items. + * + * Arguments: + * 1. jQuery Element on which you may want to created the menus + * 2. list of menu-items + * 3. categories - metadata information about the categories, based on which + * the submenu (menu-group) will be created (if any). + * 4. d - Data object for the selected browser tree item. + * 5. item - The selected browser tree item + * 6. menu_items - A empty object on which the context menu for the given + * list of menu-items. + * + * Returns if any menu generated for the given input. + */ + pgAdmin.Browser.MenuCreator = function($mnu, menus, categories, d, item, menu_items) { + var groups = {'common': []}, + common, idx = 0, j, item, + ctxId = _.uniqueId('ctx_'), + update_menuitem = function(m) { + if (m instanceof MenuItem) { + if (m.$el) { + m.$el.remove(); + delete m.$el; + } + m.generate(d, item); + var group = groups[m.category || 'common'] = + groups[m.category || 'common'] || []; + group.push(m); + } else { + for (var key in m) { + update_menuitem(m[key]); + } + } + }, ctxIdx = 1; + + for (idx in menus) { + update_menuitem(menus[idx]); + } + + // Not all menu creator requires the context menu structure. + menu_items = menu_items || {}; + + common = groups['common']; + delete groups['common']; + + var prev = true; + + for (name in groups) { + var g = groups[name], + c = categories[name] || {'label': name, single: false}, + menu_group = pgAdmin.Browser.MenuGroup(c, g, prev, ctxId); + + if (g.length <= 1 && !c.single) { + prev = false; + for (idx in g) { + common.push(g[idx]); + } + } else { + prev = g.below; + common.push(menu_group); + } + } + + // The menus will be created based on the priority given. + // Menu with lowest value has the highest priority. If the priority is + // same, then - it will be ordered by label. + // Sort by alphanumeric ordered first + common.sort(function(a, b) {return a.label.localeCompare(b.label);}); + // Sort by priority + common.sort(function(a, b) {return a.priority - b.priority;}); + var len = _.size(common); + + for (idx in common) { + item = common[idx]; + + item.priority = (item.priority || 10); + $mnu.append(item.$el); + var prefix = ctxId + '_' + item.priority + '_' + ctxIdx; + + if (ctxIdx != 1 && item.above && !item.is_disabled) { + // For creatign the seprator any string will do. + menu_items[prefix + '_ma'] = '----'; + } + + if (!item.is_disabled) { + menu_items[prefix + '_ms'] = item.context; + } + + if (ctxId != len && item.below && !item.is_disabled) { + menu_items[prefix + '_mz'] = '----'; + } + ctxIdx++; + } + + return (len > 0); + }; + // MENU PUBLIC CLASS DEFINITION // ============================== diff --git a/web/pgadmin/browser/templates/browser/css/node.css b/web/pgadmin/browser/templates/browser/css/node.css index 199423582..98895e5af 100644 --- a/web/pgadmin/browser/templates/browser/css/node.css +++ b/web/pgadmin/browser/templates/browser/css/node.css @@ -3,7 +3,7 @@ background-repeat: no-repeat; align-content: center; vertical-align: middle; - height: 1.3em; + height: 20px; } .pgadmin-node-select option[node="{{node_type}}"] { diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index b43f102a2..89fe2a841 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -44,22 +44,9 @@ try { - \n'); + $obj_mnu.empty(); // All menus (except for the object menus) are already present. // They will just require to check, wheather they are // enabled/disabled. _.each([ - {m: 'file', id: '#mnu_file'}, - {m: 'edit', id: '#mnu_edit'}, - {m: 'management', id: '#mnu_management'}, - {m: 'tools', id: '#mnu_tools'}, - {m: 'help', id:'#mnu_help'}], function(o) { - j = navbar.children(o.id).children('.dropdown-menu').first(); - _.each(obj.menus[o.m], - function(v, k) { - // Remove disabled class in any case first. - e = j.find('#' + k).closest('.menu-item').removeClass('disabled'); - if (v.disabled(d, item)) { - // Make this menu disabled - e.addClass('disabled'); - } - }); - }); + {m: 'file', id: '#mnu_file'}, + {m: 'edit', id: '#mnu_edit'}, + {m: 'management', id: '#mnu_management'}, + {m: 'tools', id: '#mnu_tools'}, + {m: 'help', id:'#mnu_help'}], function(o) { + _.each( + obj.menus[o.m], + function(m, k) { + update_menuitem(m); + }); + }); // Create the object menu dynamically - if (item && this.menus['object'] && this.menus['object'][d._type]) { - var create_items = []; - // The menus will be created based on the priority given. - // Menu with lowest value has the highest priority. - _.each(_.sortBy( - this.menus['object'][d._type], - function(o) { return o.priority; }), - function(m) { - if (m.category && m.category == 'create') { - create_items.push(m.generate(d, item)); - } else { - obj_mnu.append(m.generate(d, item)); - } - }); - // Create menus goes seperately - if (create_items.length > 0) { - create_mnu.empty(); - _.each(create_items, function(c) { - create_mnu.append(c); - }); - } + if (item && obj.menus['object'] && obj.menus['object'][d._type]) { + pgAdmin.Browser.MenuCreator( + $obj_mnu, obj.menus['object'][d._type], obj.menu_categories, d, item + ) + } else { + // Create a dummy 'no object seleted' menu + create_submenu = pgAdmin.Browser.MenuGroup( + obj.menu_categories['create'], [{ + $el: $(''), + priority: 1, + category: 'create', + update: function() {} + }], false); + $obj_mnu.append(create_submenu.$el); } }, init: function() { @@ -347,66 +353,22 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) { // Build the treeview context menu $('#tree').contextMenu({ selector: '.aciTreeLine', + autoHide: false, build: function(element) { var item = obj.tree.itemFrom(element), - menu = { }, - createMenu = { }, - d = obj.tree.itemData(item), - menus = obj.menus['context'][d._type], - cb = function(name) { - var o = undefined; + d = obj.tree.itemData(item), + menus = obj.menus['context'][d._type], + $div = $('
    '), + context_menu = {}; - _.each(menus, function(m) { - if (name == (m.module.type + '_' + m.name)) { - o = m; - } - }); + pgAdmin.Browser.MenuCreator( + $div, menus, obj.menu_categories, d, item, context_menu + ); - if (o) { - var cb; - if (o.module['callbacks'] && ( - o.callback in o.module['callbacks'])) { - cb = o.module['callbacks'][o.callback]; - } else if (o.callback in o.module) { - cb = o.module[o.callback]; - } - - if (cb) { - cb.apply(o.module, [o.data, item]); - } else { - pgAdmin.Browser.report_error( - S('Developer Warning: Callback - "%s" not found!'). - sprintf(o.cb).value()); - } - } + return { + autoHide: false, + items: context_menu }; - - _.each( - _.sortBy(menus, function(m) { return m.priority; }), - function(m) { - if (m.category == 'create' && !m.disabled(d, item)) { - createMenu[m.module.type + '_' + m.name] = { name: m.label, icon: m.icon || m.module.type }; - } - }); - - if (_.size(createMenu)) { - menu["create"] = { name: "{{ _('Create') }}", icon: 'fa fa-magic' }; - menu["create"]["items"] = createMenu; - } - - _.each( - _.sortBy(menus, function(m) { return m.priority; }), - function(m) { - if (m.category != 'create' && !m.disabled(d, item)) { - menu[m.module.type + '_' + m.name] = { name: m.label, icon: m.icon }; - } - }); - - return _.size(menu) ? { - autoHide: true, - items: menu, - callback: cb - } : {}; } }); @@ -529,6 +491,7 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) { {% endif %}{% if item.url %}url: "{{ item.url }}", {% endif %}{% if item.target %}target: "{{ item.target }}", {% endif %}{% if item.callback %}callback: "{{ item.callback }}", + {% endif %}{% if item.category %}category: "{{ item.category }}", {% endif %}{% if item.icon %}icon: '{{ item.icon }}', {% endif %}{% if item.data %}data: {{ item.data }}, {% endif %}label: '{{ item.label }}', applies: ['{{ key.lower() }}'], @@ -577,6 +540,18 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) { '{{ _('Error loading script - ') }}' + path); }); }, + add_menu_category: function( + id, label, priority, icon, above_separator, below_separator, single + ) { + this.menu_categories[id] = { + label: label, + priority: priority, + icon: icon, + above: (above_separator === true), + below: (below_separator === true), + single: single + } + }, // Add menus of module/extension at appropriate menu add_menus: function(menus) { var pgMenu = this.menus; @@ -591,7 +566,10 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) { pgMenu[a] = pgMenu[a] || {}; if (_.isString(m.node)) { menus = pgMenu[a][m.node] = pgMenu[a][m.node] || {}; - } else { + } else if (_.isString(m.category)) { + menus = pgMenu[a][m.category] = pgMenu[a][m.category] || {}; + } + else { menus = pgMenu[a]; } @@ -623,26 +601,31 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) { }, // Create the menus create_menus: function() { + /* Create menus */ var navbar = $('#navbar-menu > ul').first(); var obj = this; _.each([ - {menu: 'file', id: '#mnu_file'}, - {menu: 'edit', id: '#mnu_edit'}, - {menu: 'management', id: '#mnu_management'}, - {menu: 'tools', id: '#mnu_tools'}, - {menu: 'help', id:'#mnu_help'}], function(o) { - var j = navbar.children(o.id).children('.dropdown-menu').first().empty(); - _.each( - _.sortBy(obj.menus[o.menu], - function(v, k) { return v.priority; }), - function(v) { - j.closest('.dropdown').removeClass('hide'); - j.append(v.generate()); - }); - navbar.children('#mnu_obj').removeClass('hide'); - }); + {menu: 'file', id: '#mnu_file'}, + {menu: 'edit', id: '#mnu_edit'}, + {menu: 'management', id: '#mnu_management'}, + {menu: 'tools', id: '#mnu_tools'}, + {menu: 'help', id:'#mnu_help'}], + function(o) { + var $mnu = navbar.children(o.id).first(), + $dropdown = $mnu.children('.dropdown-menu').first(); + $dropdown.empty(); + var menus = {}; + + if (pgAdmin.Browser.MenuCreator( + $dropdown, obj.menus[o.menu], obj.menu_categories + )) { + $mnu.removeClass('hide'); + } + }); + + navbar.children('#mnu_obj').removeClass('hide'); obj.enable_disable_menus(); }, messages: { diff --git a/web/pgadmin/browser/templates/browser/js/collection.js b/web/pgadmin/browser/templates/browser/js/collection.js index 77480b1a5..74f07222c 100644 --- a/web/pgadmin/browser/templates/browser/js/collection.js +++ b/web/pgadmin/browser/templates/browser/js/collection.js @@ -29,7 +29,7 @@ function($, _, S, pgAdmin, Backbone, Alertify, Backform) { pgAdmin.Browser.add_menus([{ name: 'refresh', node: this.type, module: this, applies: ['object', 'context'], callback: 'refresh', - priority: 1, label: '{{ _("Refresh...") }}', + priority: 2, label: '{{ _("Refresh...") }}', icon: 'fa fa-refresh' }]); }, diff --git a/web/pgadmin/test/__init__.py b/web/pgadmin/test/__init__.py index c8ded795e..d131313ce 100644 --- a/web/pgadmin/test/__init__.py +++ b/web/pgadmin/test/__init__.py @@ -10,7 +10,7 @@ """Browser integration functions for the Test module.""" MODULE_NAME = 'test' from flask.ext.security import login_required -from flask import render_template, url_for, current_app +from flask import url_for, current_app from flask.ext.babel import gettext from pgadmin.utils import PgAdminModule from pgadmin.utils.menu import MenuItem @@ -22,32 +22,37 @@ class TestModule(PgAdminModule): return {'file_items': [ MenuItem(name='mnu_generate_test_html', label=gettext('Generated Test HTML'), - priority=100, + priority=2, url=url_for('test.generated')), MenuItem(name='mnu_test_alert', label=gettext('Test Alert'), priority=200, module='pgAdmin.Test', + category=gettext('alertify'), callback='test_alert'), MenuItem(name='mnu_test_confirm', label=gettext('Test Confirm'), priority=300, module='pgAdmin.Test', + category=gettext('alertify'), callback='test_confirm'), MenuItem(name='mnu_test_dialog', label=gettext('Test Dialog'), priority=400, module='pgAdmin.Test', + category=gettext('alertify'), callback='test_dialog'), MenuItem(name='mnu_test_prompt', label=gettext('Test Prompt'), priority=500, module='pgAdmin.Test', + category=gettext('alertify'), callback='test_prompt'), MenuItem(name='mnu_test_notifier', label=gettext('Test Notifier'), priority=600, module='pgAdmin.Test', + category=gettext('alertify'), callback='test_notifier'), MenuItem(name='mnu_test_disabled', label=gettext('Test Disabled'), diff --git a/web/pgadmin/test/static/js/test.js b/web/pgadmin/test/static/js/test.js index c3527fd09..aa6ec997d 100644 --- a/web/pgadmin/test/static/js/test.js +++ b/web/pgadmin/test/static/js/test.js @@ -3,6 +3,9 @@ define( function($, alertify, pgAdmin, pgServer, ServerGroup) { pgAdmin = pgAdmin || window.pgAdmin || {}; + if (pgAdmin.Test) + return pgAdmin.Test; + pgAdmin.Test = { test_alert: function() { alertify.alert( @@ -66,19 +69,31 @@ define( alertify.myAlert('Dialog Test 2', 'This is another test dialog from Alertify!', 'This is dialog 2'); + }, + init: function() { + if (this.initialized) + return; + + this.initialized = true; + + // Add the alertify category + pgAdmin.Browser.add_menu_category( + 'alertify', 'Alertify', 3, 'fa fa-film', true, true + ); + + pgServer.on( + 'server-connected', function() { + console.log(arguments); + console.log('Yay - we connected the server!'); + }, + {'a': 'test'}); + + ServerGroup.on('browser-node.loaded', function() { + console.log('I know that the server-group has been expanded!'); + }, pgAdmin.Test); + } }; - pgServer.on( - 'server-connected', function() { - console.log(arguments); - console.log('Yay - we connected the server!'); - }, - {'a': 'test'}); - - ServerGroup.on('browser-node.loaded', function() { - console.log('I know that the server-group has been expanded!'); - }, pgAdmin.Test); - return pgAdmin.Test; });