Allow to create sub-menu/menu-group by specify the same category in

multiple menu-items (not just the 'create' menu-group).

Moved all the main menu/context menu generation implementation in the
'menu' javascript menu.

In this implementation, if more than one menu-items specify same type
of categories, they will be created withing that group, otherwise - it
will be created separately (unless 'single' property of that category is
set to true).

We can also provide icon, priority, separator(s) above/below it for the
individual sub-menu too using pgAdmin.Browser.add_menu_category
function(...).
pull/3/head
Ashesh Vashi 2016-03-21 23:56:06 +05:30
parent 79d6d50dcc
commit 87f9834951
7 changed files with 395 additions and 152 deletions

View File

@ -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 = $('<a></a>', {
'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($('<i></i>', {'class': this.icon}));
}
url.append($('<span></span>').text(' ' + this.label));
return $('<li/>')
.addClass('menu-item' + (this.disabled(node, item) ? ' disabled' : ''))
this.is_disabled = this.disabled(node, item);
this.$el = $('<li/>')
.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) { %><hr><% } %>',
'<li class="menu-item dropdown dropdown-submenu">',
' <a href="#" class="dropdown-toggle" data-toggle="dropdown">',
' <% if (icon) { %><i class="<%= icon %>"></i><% } %>',
' <span><%= label %></span>',
' </a>',
' <ul class="dropdown-menu navbar-inverse">',
' </ul>',
'</li>',
'<% if (below) { %><hr><% } %>',].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
// ==============================

View File

@ -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}}"] {

View File

@ -44,22 +44,9 @@ try {
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Edit') }} <span class="caret"></span></a>
<ul class="dropdown-menu navbar-inverse" role="menu"></ul>
</li>
<li id="mnu_obj" class="dropdown hide">
<li id="mnu_obj" class="dropdown ">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Object') }} <span class="caret"></span></a>
<ul id="mnu_dropdown_obj" class="dropdown-menu navbar-inverse" role="menu">
<li id="mnu_create_dropdown_obj" class="menu-item dropdown dropdown-submenu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-magic"></i>
<span>{{ _('Create') }}</span>
</a>
<ul id="mnu_create_obj" class="dropdown-menu navbar-inverse">
<li class="menu-item">
<a href="#">Link 1</a>
</li>
</ul>
</li>
<hr/>
</ul>
<ul class="dropdown-menu navbar-inverse" role="menu"></ul>
</li>
<li id="mnu_management" class="dropdown hide">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ _('Management') }} <span class="caret"></span></a>

View File

@ -160,6 +160,18 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) {
// Help menus
help: {}
},
menu_categories: {
/* name, label (pair) */
'create': {
label: '{{ _('Create')|safe }}',
priority: 1,
/* separator above this menu */
above: false,
below: true,
icon: 'fa fa-magic',
single: true
}
},
// A callback to load/fetch a script when a certain node is loaded
register_script: function(n, m, p) {
var scripts = this.scripts;
@ -186,63 +198,57 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) {
enable_disable_menus: function(item) {
// Mechanism to enable/disable menus depending on the condition.
var obj = this, j, e,
// menu navigation bar
navbar = $('#navbar-menu > ul').first(),
// Drop down menu for objects
obj_mnu = navbar.find('li#mnu_obj > ul#mnu_dropdown_obj').first(),
// Drop down menu for create object
create_mnu = navbar.find("#mnu_create_obj").empty(),
// data for current selected object
d = this.tree.itemData(item);
// menu navigation bar
navbar = $('#navbar-menu > ul').first(),
// Drop down menu for objects
$obj_mnu = navbar.find('li#mnu_obj > ul.dropdown-menu').first(),
// data for current selected object
d = obj.tree.itemData(item),
update_menuitem = function(m) {
if (m instanceof pgAdmin.Browser.MenuItem) {
m.update(d, item);
} else {
for (var key in m) {
update_menuitem(m[key]);
}
}
};
// All menus from the object menus (except the create drop-down
// menu) needs to be removed.
obj_mnu.children("li:not(:first-child)").remove();
// Create a dummy 'no object seleted' menu
create_mnu.html('<li class="menu-item disabled"><a href="#">{{ _('No object selected') }}</a></li>\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: $('<li class="menu-item disabled"><a href="#">{{ _("No object selected") }}</a></li>'),
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 = $('<div></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: {

View File

@ -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'
}]);
},

View File

@ -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'),

View File

@ -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;
});