From ea47f96a66d9ec7bd5ed06e13a3520b10652b488 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 17 Dec 2013 10:58:38 +0100 Subject: [PATCH] Wizard UI for Workflow This BP implements a wizard UI for modal workflow dialog. implements bp wizard-ui-for-workflow Change-Id: Ica1a7e085b016417d285eb6355e70836f68ac170 --- horizon/static/horizon/js/horizon.modals.js | 109 ++++++++ .../lib/jquery/jquery.bootstrap.wizard.js | 255 ++++++++++++++++++ horizon/templates/horizon/_scripts.html | 2 +- .../templates/horizon/common/_workflow.html | 18 +- horizon/workflows/base.py | 5 + horizon/workflows/views.py | 37 +++ .../dashboards/project/networks/workflows.py | 1 + .../static/bootstrap/less/pager.less | 23 +- .../static/dashboard/less/horizon.less | 21 -- .../dashboard/less/horizon_workflow.less | 88 ++++++ .../templates/_stylesheets.html | 1 + 11 files changed, 529 insertions(+), 31 deletions(-) create mode 100755 horizon/static/horizon/lib/jquery/jquery.bootstrap.wizard.js create mode 100644 openstack_dashboard/static/dashboard/less/horizon_workflow.less diff --git a/horizon/static/horizon/js/horizon.modals.js b/horizon/static/horizon/js/horizon.modals.js index ea4fc1ffb3..8572473ae2 100644 --- a/horizon/static/horizon/js/horizon.modals.js +++ b/horizon/static/horizon/js/horizon.modals.js @@ -155,6 +155,115 @@ horizon.addInitFunction(function() { $(modal).find(":text, select, textarea").filter(":visible:first").focus(); }); + // If workflow id wizard mode, initialize wizard. + horizon.modals.addModalInitFunction(function (modal) { + var _max_visited_step = 0; + var _validate_steps = function (start, end) { + var $form = $('.workflow > form'), + response = {}; + + if (typeof end === 'undefined') { + end = start; + } + + // Clear old errors. + $form.find('td.actions div.alert-error').remove(); + $form.find('.control-group.error').each(function () { + var $group = $(this); + $group.removeClass('error'); + $group.find('span.help-inline.error').remove(); + }); + + // Send the data for validation. + $.ajax({ + type: 'POST', + url: $form.attr('action'), + headers: { + 'X-Horizon-Validate-Step-Start': start, + 'X-Horizon-Validate-Step-End': end + }, + data: $form.serialize(), + dataType: 'json', + async: false, + success: function (data) { response = data; } + }); + + // Handle errors. + if (response.has_errors) { + var first_field = true; + + $.each(response.errors, function (step_slug, step_errors) { + var step_id = response.workflow_slug + '__' + step_slug, + $fieldset = $form.find('#' + step_id); + $.each(step_errors, function (field, errors) { + var $field; + if (field === '__all__') { + // Add global errors. + $.each(errors, function (index, error) { + $fieldset.find('td.actions').prepend( + '
' + + error + '
'); + }); + $fieldset.find('input, select, textarea').first().focus(); + return; + } + // Add field errors. + $field = $fieldset.find('[name="' + field + '"]'); + $field.closest('.control-group').addClass('error'); + $.each(errors, function (index, error) { + $field.before( + '' + + error + ''); + }); + // Focus the first invalid field. + if (first_field) { + $field.focus(); + first_field = false; + } + }); + }); + + return false; + } + }; + + $('.workflow.wizard').bootstrapWizard({ + tabClass: 'wizard-tabs', + nextSelector: '.button-next', + previousSelector: '.button-previous', + onTabShow: function (tab, navigation, index) { + var $navs = navigation.find('li'); + var total = $navs.length; + var current = index; + var $footer = $('.modal-footer'); + _max_visited_step = Math.max(_max_visited_step, current); + if (current + 1 >= total) { + $footer.find('.button-next').hide(); + $footer.find('.button-final').show(); + } else { + $footer.find('.button-next').show(); + $footer.find('.button-final').hide(); + } + $navs.each(function(i) { + $this = $(this); + if (i <= _max_visited_step) { + $this.addClass('done'); + } else { + $this.removeClass('done'); + } + }); + }, + onNext: function ($tab, $nav, index) { + return _validate_steps(index - 1); + }, + onTabClick: function ($tab, $nav, current, index) { + // Validate if moving forward, but move backwards without validation + return (index <= current || + _validate_steps(current, index - 1) !== false); + } + }); + }); + horizon.modals.addModalInitFunction(horizon.datatables.validate_button); // Load modals for ajax-modal links. diff --git a/horizon/static/horizon/lib/jquery/jquery.bootstrap.wizard.js b/horizon/static/horizon/lib/jquery/jquery.bootstrap.wizard.js new file mode 100755 index 0000000000..fd9ffc0aee --- /dev/null +++ b/horizon/static/horizon/lib/jquery/jquery.bootstrap.wizard.js @@ -0,0 +1,255 @@ +/*! + * jQuery twitter bootstrap wizard plugin + * Examples and documentation at: http://github.com/VinceG/twitter-bootstrap-wizard + * version 1.0 + * Requires jQuery v1.3.2 or later + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * Authors: Vadim Vincent Gabriel (http://vadimg.com), Jason Gill (www.gilluminate.com) + */ +;(function($) { +var bootstrapWizardCreate = function(element, options) { + var element = $(element); + var obj = this; + + // Merge options with defaults + var $settings = $.extend({}, $.fn.bootstrapWizard.defaults, options); + var $activeTab = null; + var $navigation = null; + + this.rebindClick = function(selector, fn) + { + selector.unbind('click', fn).bind('click', fn); + } + + this.fixNavigationButtons = function() { + // Get the current active tab + if(!$activeTab.length) { + // Select first one + $navigation.find('a:first').tab('show'); + $activeTab = $navigation.find('li:first'); + } + + // See if we're currently in the first/last then disable the previous and last buttons + $($settings.previousSelector, element).toggleClass('disabled', (obj.firstIndex() >= obj.currentIndex())); + $($settings.nextSelector, element).toggleClass('disabled', (obj.currentIndex() >= obj.navigationLength())); + + // We are unbinding and rebinding to ensure single firing and no double-click errors + obj.rebindClick($($settings.nextSelector, element), obj.next); + obj.rebindClick($($settings.previousSelector, element), obj.previous); + obj.rebindClick($($settings.lastSelector, element), obj.last); + obj.rebindClick($($settings.firstSelector, element), obj.first); + + if($settings.onTabShow && typeof $settings.onTabShow === 'function' && $settings.onTabShow($activeTab, $navigation, obj.currentIndex())===false){ + return false; + } + }; + + this.next = function(e) { + + // If we clicked the last then dont activate this + if(element.hasClass('last')) { + return false; + } + + if($settings.onNext && typeof $settings.onNext === 'function' && $settings.onNext($activeTab, $navigation, obj.nextIndex())===false){ + return false; + } + + // Did we click the last button + $index = obj.nextIndex(); + if($index > obj.navigationLength()) { + } else { + $navigation.find('li:eq('+$index+') a').tab('show'); + } + }; + + this.previous = function(e) { + + // If we clicked the first then dont activate this + if(element.hasClass('first')) { + return false; + } + + if($settings.onPrevious && typeof $settings.onPrevious === 'function' && $settings.onPrevious($activeTab, $navigation, obj.previousIndex())===false){ + return false; + } + + $index = obj.previousIndex(); + if($index < 0) { + } else { + $navigation.find('li:eq('+$index+') a').tab('show'); + } + }; + + this.first = function(e) { + if($settings.onFirst && typeof $settings.onFirst === 'function' && $settings.onFirst($activeTab, $navigation, obj.firstIndex())===false){ + return false; + } + + // If the element is disabled then we won't do anything + if(element.hasClass('disabled')) { + return false; + } + $navigation.find('li:eq(0) a').tab('show'); + + }; + this.last = function(e) { + if($settings.onLast && typeof $settings.onLast === 'function' && $settings.onLast($activeTab, $navigation, obj.lastIndex())===false){ + return false; + } + + // If the element is disabled then we won't do anything + if(element.hasClass('disabled')) { + return false; + } + $navigation.find('li:eq('+obj.navigationLength()+') a').tab('show'); + }; + this.currentIndex = function() { + return $navigation.find('li').index($activeTab); + }; + this.firstIndex = function() { + return 0; + }; + this.lastIndex = function() { + return obj.navigationLength(); + }; + this.getIndex = function(e) { + return $navigation.find('li').index(e); + }; + this.nextIndex = function() { + return $navigation.find('li').index($activeTab) + 1; + }; + this.previousIndex = function() { + return $navigation.find('li').index($activeTab) - 1; + }; + this.navigationLength = function() { + return $navigation.find('li').length - 1; + }; + this.activeTab = function() { + return $activeTab; + }; + this.nextTab = function() { + return $navigation.find('li:eq('+(obj.currentIndex()+1)+')').length ? $navigation.find('li:eq('+(obj.currentIndex()+1)+')') : null; + }; + this.previousTab = function() { + if(obj.currentIndex() <= 0) { + return null; + } + return $navigation.find('li:eq('+parseInt(obj.currentIndex()-1)+')'); + }; + this.show = function(index) { + return element.find('li:eq(' + index + ') a').tab('show'); + }; + this.disable = function(index) { + $navigation.find('li:eq('+index+')').addClass('disabled'); + }; + this.enable = function(index) { + $navigation.find('li:eq('+index+')').removeClass('disabled'); + }; + this.hide = function(index) { + $navigation.find('li:eq('+index+')').hide(); + }; + this.display = function(index) { + $navigation.find('li:eq('+index+')').show(); + }; + this.remove = function(args) { + var $index = args[0]; + var $removeTabPane = typeof args[1] != 'undefined' ? args[1] : false; + var $item = $navigation.find('li:eq('+$index+')'); + + // Remove the tab pane first if needed + if($removeTabPane) { + var $href = $item.find('a').attr('href'); + $($href).remove(); + } + + // Remove menu item + $item.remove(); + }; + + $navigation = element.find('ul:first', element); + $activeTab = $navigation.find('li.active', element); + + if(!$navigation.hasClass($settings.tabClass)) { + $navigation.addClass($settings.tabClass); + } + + // Load onInit + if($settings.onInit && typeof $settings.onInit === 'function'){ + $settings.onInit($activeTab, $navigation, 0); + } + + // Load onShow + if($settings.onShow && typeof $settings.onShow === 'function'){ + $settings.onShow($activeTab, $navigation, obj.nextIndex()); + } + + // Work the next/previous buttons + obj.fixNavigationButtons(); + + $('a[data-toggle="tab"]', $navigation).on('click', function (e) { + // Get the index of the clicked tab + var clickedIndex = $navigation.find('li').index($(e.currentTarget).parent('li')); + if($settings.onTabClick && typeof $settings.onTabClick === 'function' && $settings.onTabClick($activeTab, $navigation, obj.currentIndex(), clickedIndex)===false){ + return false; + } + }); + + $('a[data-toggle="tab"]', $navigation).on('shown', function (e) { // use shown instead of show to help prevent double firing + $element = $(e.target).parent(); + var nextTab = $navigation.find('li').index($element); + + // If it's disabled then do not change + if($element.hasClass('disabled')) { + return false; + } + + if($settings.onTabChange && typeof $settings.onTabChange === 'function' && $settings.onTabChange($activeTab, $navigation, obj.currentIndex(), nextTab)===false){ + return false; + } + + $activeTab = $element; // activated tab + obj.fixNavigationButtons(); + }); +}; +$.fn.bootstrapWizard = function(options) { + //expose methods + if (typeof options == 'string') { + var args = Array.prototype.slice.call(arguments, 1) + if(args.length === 1) { + args.toString(); + } + return this.data('bootstrapWizard')[options](args); + } + return this.each(function(index){ + var element = $(this); + // Return early if this element already has a plugin instance + if (element.data('bootstrapWizard')) return; + // pass options to plugin constructor + var wizard = new bootstrapWizardCreate(element, options); + // Store plugin object in this element's data + element.data('bootstrapWizard', wizard); + }); +}; + +// expose options +$.fn.bootstrapWizard.defaults = { + tabClass: 'nav nav-pills', + nextSelector: '.wizard li.next', + previousSelector: '.wizard li.previous', + firstSelector: '.wizard li.first', + lastSelector: '.wizard li.last', + onShow: null, + onInit: null, + onNext: null, + onPrevious: null, + onLast: null, + onFirst: null, + onTabChange: null, + onTabClick: null, + onTabShow: null +}; + +})(jQuery); diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html index d2fef050b1..ff22fa915c 100644 --- a/horizon/templates/horizon/_scripts.html +++ b/horizon/templates/horizon/_scripts.html @@ -19,13 +19,13 @@ + - diff --git a/horizon/templates/horizon/common/_workflow.html b/horizon/templates/horizon/common/_workflow.html index d2ef8b32e7..4729ced2ef 100644 --- a/horizon/templates/horizon/common/_workflow.html +++ b/horizon/templates/horizon/common/_workflow.html @@ -1,6 +1,6 @@ {% load i18n %} {% with workflow.get_entry_point as entry_point %} -