Merge "Wizard UI for Workflow"
This commit is contained in:
commit
21dbd64376
@ -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(
|
||||
'<div class="alert alert-message alert-error">' +
|
||||
error + '</div>');
|
||||
});
|
||||
$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(
|
||||
'<span class="help-inline error">' +
|
||||
error + '</span>');
|
||||
});
|
||||
// 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.
|
||||
|
255
horizon/static/horizon/lib/jquery/jquery.bootstrap.wizard.js
Executable file
255
horizon/static/horizon/lib/jquery/jquery.bootstrap.wizard.js
Executable file
@ -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);
|
@ -19,13 +19,13 @@
|
||||
<script src="{{ STATIC_URL }}horizon/lib/spin.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ STATIC_URL }}horizon/lib/spin.jquery.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery-ui-1.9.2.custom.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.bootstrap.wizard.js" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
<script src="{{ STATIC_URL }}horizon/lib/d3.v3.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
<script src="{{ STATIC_URL }}bootstrap/js/bootstrap.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src='{{ STATIC_URL }}bootstrap/js/bootstrap-datepicker.js' type='text/javascript' charset='utf-8'></script>
|
||||
|
||||
|
||||
<script src="{{ STATIC_URL }}horizon/lib/hogan-2.0.0.js" type="text/javascript" charset='utf-8'></script>
|
||||
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.communication.js' type='text/javascript' charset='utf-8'></script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% with workflow.get_entry_point as entry_point %}
|
||||
<div class="workflow {% if modal %}modal hide{% else %}static_page{% endif %}">
|
||||
<div class="workflow {% if modal %}modal hide{% else %}static_page{% endif %}{% if workflow.wizard %} wizard{% endif %}">
|
||||
<form {{ workflow.attr_string|safe }} action="{{ workflow.get_absolute_url }}" {% if add_to_field %}data-add-to-field="{{ add_to_field }}"{% endif %} method="POST"{% if workflow.multipart %} enctype="multipart/form-data"{% endif %}>{% csrf_token %}
|
||||
{% if REDIRECT_URL %}<input type="hidden" name="{{ workflow.redirect_param_name }}" value="{{ REDIRECT_URL }}"/>{% endif %}
|
||||
<div class="modal-header">
|
||||
@ -32,8 +32,20 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{{ workflow.finalize_button_name }}" />
|
||||
{% if modal %}<a class="btn secondary cancel close">{% trans "Cancel" %}</a>{% endif %}
|
||||
{% if workflow.wizard %}
|
||||
<div class="row-fluid">
|
||||
<div class="span6 back">
|
||||
<button type="button" class="btn button-previous">« {% trans "Back" %}</button>
|
||||
</div>
|
||||
<div class="span6 next">
|
||||
<button type="button" class="btn btn-primary button-next">{% trans "Next" %} »</button>
|
||||
<button type="submit" class="btn btn-primary button-final">{{ workflow.finalize_button_name }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{{ workflow.finalize_button_name }}" />
|
||||
{% if modal %}<a class="btn secondary cancel close">{% trans "Cancel" %}</a>{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -576,6 +576,10 @@ class Workflow(html.HTMLElement):
|
||||
the case of a workflow which updates a resource it would be the
|
||||
resource being updated after it has been retrieved.
|
||||
|
||||
.. attribute:: wizard
|
||||
|
||||
Whether to present the workflow as a wizard, with "prev" and "next"
|
||||
buttons and validation after every step.
|
||||
"""
|
||||
__metaclass__ = WorkflowMetaclass
|
||||
slug = None
|
||||
@ -586,6 +590,7 @@ class Workflow(html.HTMLElement):
|
||||
failure_message = _("%s did not complete.")
|
||||
redirect_param_name = "next"
|
||||
multipart = False
|
||||
wizard = False
|
||||
_registerable_class = Step
|
||||
|
||||
def __unicode__(self):
|
||||
|
@ -124,10 +124,47 @@ class WorkflowView(generic.TemplateView):
|
||||
self.set_workflow_step_errors(context)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def validate_steps(self, request, workflow, start, end):
|
||||
"""Validates the workflow steps from ``start`` to ``end``, inclusive.
|
||||
|
||||
Returns a dict describing the validation state of the workflow.
|
||||
"""
|
||||
errors = {}
|
||||
for step in workflow.steps[start:end + 1]:
|
||||
if not step.action.is_valid():
|
||||
errors[step.slug] = dict(
|
||||
(field, [unicode(error) for error in errors])
|
||||
for (field, errors) in step.action.errors.iteritems())
|
||||
return {
|
||||
'has_errors': bool(errors),
|
||||
'workflow_slug': workflow.slug,
|
||||
'errors': errors,
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Handler for HTTP POST requests."""
|
||||
context = self.get_context_data(**kwargs)
|
||||
workflow = context[self.context_object_name]
|
||||
try:
|
||||
# Check for the VALIDATE_STEP* headers, if they are present
|
||||
# and valid integers, return validation results as JSON,
|
||||
# otherwise proceed normally.
|
||||
validate_step_start = int(self.request.META.get(
|
||||
'HTTP_X_HORIZON_VALIDATE_STEP_START', ''))
|
||||
validate_step_end = int(self.request.META.get(
|
||||
'HTTP_X_HORIZON_VALIDATE_STEP_END', ''))
|
||||
except ValueError:
|
||||
# No VALIDATE_STEP* headers, or invalid values. Just proceed
|
||||
# with normal workflow handling for POSTs.
|
||||
pass
|
||||
else:
|
||||
# There are valid VALIDATE_STEP* headers, so only do validation
|
||||
# for the specified steps and return results.
|
||||
data = self.validate_steps(request, workflow,
|
||||
validate_step_start,
|
||||
validate_step_end)
|
||||
return http.HttpResponse(json.dumps(data),
|
||||
mimetype="application/json")
|
||||
if workflow.is_valid():
|
||||
try:
|
||||
success = workflow.finalize()
|
||||
|
@ -271,6 +271,7 @@ class CreateNetwork(workflows.Workflow):
|
||||
default_steps = (CreateNetworkInfo,
|
||||
CreateSubnetInfo,
|
||||
CreateSubnetDetail)
|
||||
wizard = True
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("horizon:project:networks:index")
|
||||
|
@ -2,8 +2,7 @@
|
||||
// -----
|
||||
|
||||
.pager {
|
||||
margin-left: 0;
|
||||
margin-bottom: @baseLineHeight;
|
||||
margin: @baseLineHeight 0;
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
.clearfix();
|
||||
@ -11,20 +10,32 @@
|
||||
.pager li {
|
||||
display: inline;
|
||||
}
|
||||
.pager a {
|
||||
.pager li > a,
|
||||
.pager li > span {
|
||||
display: inline-block;
|
||||
padding: 5px 14px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
.border-radius(15px);
|
||||
}
|
||||
.pager a:hover {
|
||||
.pager li > a:hover,
|
||||
.pager li > a:focus {
|
||||
text-decoration: none;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.pager .next a {
|
||||
.pager .next > a,
|
||||
.pager .next > span {
|
||||
float: right;
|
||||
}
|
||||
.pager .previous a {
|
||||
.pager .previous > a,
|
||||
.pager .previous > span {
|
||||
float: left;
|
||||
}
|
||||
.pager .disabled > a,
|
||||
.pager .disabled > a:hover,
|
||||
.pager .disabled > a:focus,
|
||||
.pager .disabled > span {
|
||||
color: @grayLight;
|
||||
background-color: #fff;
|
||||
cursor: default;
|
||||
}
|
@ -1014,27 +1014,6 @@ form.horizontal fieldset {
|
||||
width: 308px;
|
||||
}
|
||||
|
||||
.workflow ul.nav-tabs {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.workflow td.actions {
|
||||
vertical-align: top;
|
||||
width: 308px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.workflow td.help_text {
|
||||
vertical-align: top;
|
||||
width: 340px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.workflow fieldset > table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
width: 0;
|
||||
|
@ -0,0 +1,88 @@
|
||||
.workflow ul.nav-tabs {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.workflow td.actions {
|
||||
vertical-align: top;
|
||||
width: 308px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.workflow td.help_text {
|
||||
vertical-align: top;
|
||||
width: 340px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.workflow fieldset > table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.workflow.wizard {
|
||||
.row-fluid .btn {
|
||||
float: none;
|
||||
margin: 0;
|
||||
}
|
||||
.row-fluid .next {
|
||||
text-align: right;
|
||||
}
|
||||
.nav-tabs.wizard-tabs {
|
||||
border-bottom: none;
|
||||
background: #efefef;
|
||||
padding: 0;
|
||||
}
|
||||
.nav-tabs.wizard-tabs li {
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-tabs.wizard-tabs li:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -20px;
|
||||
z-index: 2;
|
||||
display: block;
|
||||
border: 20px solid transparent;
|
||||
border-right: 0;
|
||||
border-left: 20px solid #ddd;
|
||||
content:"";
|
||||
}
|
||||
.nav-tabs.wizard-tabs li:after {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -22px;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
border: 22px solid transparent;
|
||||
border-right: 0;
|
||||
border-left: 22px solid #fff;
|
||||
content:"";
|
||||
}
|
||||
.nav-tabs.wizard-tabs li.active:before, .nav-tabs.wizard-tabs li.done:before {
|
||||
border-left: 20px solid #007acc;
|
||||
}
|
||||
.nav-tabs.wizard-tabs li a {
|
||||
border:none;
|
||||
border-radius: 0;
|
||||
background: #ddd;
|
||||
color: #43a4d7;
|
||||
padding: 0 15px;
|
||||
padding-left: 35px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
.nav-tabs.wizard-tabs li:first-child a {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.nav-tabs.wizard-tabs li.active a {
|
||||
background: #007acc;
|
||||
color:#fff;
|
||||
}
|
||||
.nav-tabs.wizard-tabs li.done a {
|
||||
background: #007acc;
|
||||
}
|
||||
label.error {
|
||||
color:#f00;
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
<link href='{{ STATIC_URL }}dashboard/less/horizon.less' type='text/less' media='screen' rel='stylesheet' />
|
||||
<link href='{{ STATIC_URL }}dashboard/less/rickshaw.css' type='text/css' media='screen' rel='stylesheet' />
|
||||
<link href='{{ STATIC_URL }}dashboard/less/horizon_charts.less' type='text/less' media='screen' rel='stylesheet' />
|
||||
<link href='{{ STATIC_URL }}dashboard/less/horizon_workflow.less' type='text/less' media='screen' rel='stylesheet' />
|
||||
{% endcompress %}
|
||||
|
||||
<link rel="shortcut icon" href="{{ STATIC_URL }}dashboard/img/favicon.ico"/>
|
||||
|
Loading…
Reference in New Issue
Block a user