diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 692ec695ce..98a774fcc2 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -92,7 +92,7 @@ def press_the_notification_button(_step, name):
# the "Save" button at the UI level.
# Instead, we use JavaScript to reliably click
# the button.
- btn_css = 'div#page-notification a.action-%s' % name.lower()
+ btn_css = 'div#page-notification button.action-%s' % name.lower()
world.trigger_event(btn_css, event='focus')
world.browser.execute_script("$('{}').click()".format(btn_css))
world.wait_for_ajax_complete()
@@ -284,7 +284,7 @@ def button_disabled(step, value):
def _do_studio_prompt_action(intent, action):
"""
Wait for a studio prompt to appear and press the specified action button
- See cms/static/js/views/feedback_prompt.js for implementation
+ See common/js/components/views/feedback_prompt.js for implementation
"""
assert intent in [
'warning',
@@ -299,7 +299,7 @@ def _do_studio_prompt_action(intent, action):
world.wait_for_present('div.wrapper-prompt.is-shown#prompt-{}'.format(intent))
- action_css = 'li.nav-item > a.action-{}'.format(action)
+ action_css = 'li.nav-item > button.action-{}'.format(action)
world.trigger_event(action_css, event='focus')
world.browser.execute_script("$('{}').click()".format(action_css))
diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py
index 1d3f7e3a47..c2f62e69e4 100644
--- a/cms/djangoapps/contentstore/features/component.py
+++ b/cms/djangoapps/contentstore/features/component.py
@@ -97,7 +97,7 @@ def delete_components(step, number):
world.wait_for_xmodule()
delete_btn_css = 'a.delete-button'
prompt_css = 'div#prompt-warning'
- btn_css = '{} a.button.action-primary'.format(prompt_css)
+ btn_css = '{} button.action-primary'.format(prompt_css)
saving_mini_css = 'div#page-notification .wrapper-notification-mini'
for _ in range(int(number)):
world.css_click(delete_btn_css)
diff --git a/cms/djangoapps/contentstore/features/course-export.py b/cms/djangoapps/contentstore/features/course-export.py
index 38f2dc5c32..5a0867565d 100644
--- a/cms/djangoapps/contentstore/features/course-export.py
+++ b/cms/djangoapps/contentstore/features/course-export.py
@@ -49,8 +49,7 @@ def get_an_error_dialog(step):
@step('I can click to go to the unit with the error$')
def i_click_on_error_dialog(step):
- world.wait_for_visible(".button.action-primary")
- world.click_link_by_text('Correct failed component')
+ world.css_click("button.action-primary")
problem_string = unicode(world.scenario_dict['COURSE'].id.make_usage_key("problem", 'ignore'))
problem_string = u"Problem {}".format(problem_string[:problem_string.rfind('ignore')])
diff --git a/cms/djangoapps/contentstore/features/course-outline.py b/cms/djangoapps/contentstore/features/course-outline.py
index 6ab7f85e81..702364b1a0 100644
--- a/cms/djangoapps/contentstore/features/course-outline.py
+++ b/cms/djangoapps/contentstore/features/course-outline.py
@@ -75,7 +75,7 @@ def i_press_the_section_delete_icon(step):
@step(u'I will confirm all alerts')
def i_confirm_all_alerts(step):
- confirm_locator = '.prompt .nav-actions a.action-primary'
+ confirm_locator = '.prompt .nav-actions button.action-primary'
world.css_click(confirm_locator)
diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee
index 8bbae2219e..b211eb3fdf 100644
--- a/cms/static/coffee/spec/main.coffee
+++ b/cms/static/coffee/spec/main.coffee
@@ -220,7 +220,6 @@ define([
"coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec",
"coffee/spec/models/upload_spec",
- "coffee/spec/views/course_info_spec", "coffee/spec/views/feedback_spec",
"coffee/spec/views/metadata_edit_spec", "coffee/spec/views/module_edit_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
@@ -254,8 +253,6 @@ define([
"js/spec/views/license_spec",
"js/spec/views/paging_spec",
- "js/spec/views/utils/view_utils_spec",
-
"js/spec/views/pages/container_spec",
"js/spec/views/pages/container_subviews_spec",
"js/spec/views/pages/group_configurations_spec",
diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee
index 45cbd52bbc..66e8048889 100644
--- a/cms/static/coffee/spec/main_spec.coffee
+++ b/cms/static/coffee/spec/main_spec.coffee
@@ -21,10 +21,8 @@ require ["jquery", "backbone", "coffee/src/main", "common/js/spec_helpers/ajax_h
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
describe "AJAX Errors", ->
- tpl = readFixtures('system-feedback.underscore')
beforeEach ->
- setFixtures($("
- ## js templates
-
-
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
diff --git a/cms/static/js/views/utils/view_utils.js b/common/static/common/js/components/utils/view_utils.js
similarity index 97%
rename from cms/static/js/views/utils/view_utils.js
rename to common/static/common/js/components/utils/view_utils.js
index efb3ae7f71..43761b2cb0 100644
--- a/cms/static/js/views/utils/view_utils.js
+++ b/common/static/common/js/components/utils/view_utils.js
@@ -1,7 +1,10 @@
/**
* Provides useful utilities for views.
*/
-define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt"],
+;(function (define) {
+ 'use strict';
+ define(["jquery", "underscore", "gettext", "common/js/components/views/feedback_notification",
+ "common/js/components/views/feedback_prompt"],
function ($, _, gettext, NotificationView, PromptView) {
var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset,
@@ -246,3 +249,4 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
'checkTotalKeyLengthViolations': checkTotalKeyLengthViolations
};
});
+}).call(this, define || RequireJS.define);
diff --git a/common/static/common/js/components/views/feedback.js b/common/static/common/js/components/views/feedback.js
new file mode 100644
index 0000000000..d6f4cde749
--- /dev/null
+++ b/common/static/common/js/components/views/feedback.js
@@ -0,0 +1,152 @@
+;(function (define) {
+ 'use strict';
+ define(["jquery",
+ "underscore",
+ "underscore.string",
+ "backbone",
+ "text!common/templates/components/system-feedback.underscore"],
+ function($, _, str, Backbone, systemFeedbackTemplate) {
+ var SystemFeedback = Backbone.View.extend({
+ options: {
+ title: "",
+ message: "",
+ intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
+ type: null, // "alert", "notification", or "prompt": set by subclass
+ shown: true, // is this view currently being shown?
+ icon: true, // should we render an icon related to the message intent?
+ closeIcon: true, // should we render a close button in the top right corner?
+ minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
+ maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
+
+ /* Could also have an "actions" hash: here is an example demonstrating
+ the expected structure. For each action, by default the framework
+ will call preventDefault on the click event before the function is
+ run; to make it not do that, just pass `preventDefault: false` in
+ the action object.
+
+ actions: {
+ primary: {
+ "text": "Save",
+ "class": "action-save",
+ "click": function(view) {
+ // do something when Save is clicked
+ }
+ },
+ secondary: [
+ {
+ "text": "Cancel",
+ "class": "action-cancel",
+ "click": function(view) {}
+ }, {
+ "text": "Discard Changes",
+ "class": "action-discard",
+ "click": function(view) {}
+ }
+ ]
+ }
+ */
+ },
+
+ initialize: function() {
+ if (!this.options.type) {
+ throw "SystemFeedback: type required (given " +
+ JSON.stringify(this.options) + ")";
+ }
+ if (!this.options.intent) {
+ throw "SystemFeedback: intent required (given " +
+ JSON.stringify(this.options) + ")";
+ }
+ this.setElement($("#page-" + this.options.type));
+ // handle single "secondary" action
+ if (this.options.actions && this.options.actions.secondary &&
+ !_.isArray(this.options.actions.secondary)) {
+ this.options.actions.secondary = [this.options.actions.secondary];
+ }
+ return this;
+ },
+
+ // public API: show() and hide()
+ show: function() {
+ clearTimeout(this.hideTimeout);
+ this.options.shown = true;
+ this.shownAt = new Date();
+ this.render();
+ if ($.isNumeric(this.options.maxShown)) {
+ this.hideTimeout = setTimeout(_.bind(this.hide, this),
+ this.options.maxShown);
+ }
+ return this;
+ },
+
+ hide: function() {
+ if (this.shownAt && $.isNumeric(this.options.minShown) &&
+ this.options.minShown > new Date() - this.shownAt) {
+ clearTimeout(this.hideTimeout);
+ this.hideTimeout = setTimeout(_.bind(this.hide, this),
+ this.options.minShown - (new Date() - this.shownAt));
+ } else {
+ this.options.shown = false;
+ delete this.shownAt;
+ this.render();
+ }
+ return this;
+ },
+
+ // the rest of the API should be considered semi-private
+ events: {
+ "click .action-close": "hide",
+ "click .action-primary": "primaryClick",
+ "click .action-secondary": "secondaryClick"
+ },
+
+ render: function() {
+ // there can be only one active view of a given type at a time: only
+ // one alert, only one notification, only one prompt. Therefore, we'll
+ // use a singleton approach.
+ var singleton = SystemFeedback["active_" + this.options.type];
+ if (singleton && singleton !== this) {
+ singleton.stopListening();
+ singleton.undelegateEvents();
+ }
+ this.$el.html(_.template(systemFeedbackTemplate)(this.options));
+ SystemFeedback["active_" + this.options.type] = this;
+ return this;
+ },
+
+ primaryClick: function(event) {
+ var actions, primary;
+ actions = this.options.actions;
+ if (!actions) { return; }
+ primary = actions.primary;
+ if (!primary) { return; }
+ if (primary.preventDefault !== false) {
+ event.preventDefault();
+ }
+ if (primary.click) {
+ primary.click.call(event.target, this, event);
+ }
+ },
+
+ secondaryClick: function(event) {
+ var actions, secondaryList, secondary, i;
+ actions = this.options.actions;
+ if (!actions) { return; }
+ secondaryList = actions.secondary;
+ if (!secondaryList) { return; }
+ // which secondary action was clicked?
+ i = 0; // default to the first secondary action (easier for testing)
+ if (event && event.target) {
+ i = _.indexOf(this.$(".action-secondary"), event.target);
+ }
+ secondary = secondaryList[i];
+ if (secondary.preventDefault !== false) {
+ event.preventDefault();
+ }
+ if (secondary.click) {
+ secondary.click.call(event.target, this, event);
+ }
+ }
+ });
+ return SystemFeedback;
+ });
+}).call(this, define || RequireJS.define);
diff --git a/common/static/common/js/components/views/feedback_alert.js b/common/static/common/js/components/views/feedback_alert.js
new file mode 100644
index 0000000000..8f3f716a58
--- /dev/null
+++ b/common/static/common/js/components/views/feedback_alert.js
@@ -0,0 +1,42 @@
+;(function (define) {
+ 'use strict';
+ define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"],
+ function($, _, str, SystemFeedbackView) {
+ str = str || _.str;
+ var Alert = SystemFeedbackView.extend({
+ options: $.extend({}, SystemFeedbackView.prototype.options, {
+ type: "alert"
+ }),
+ slide_speed: 900,
+ show: function() {
+ SystemFeedbackView.prototype.show.apply(this, arguments);
+ this.$el.hide();
+ this.$el.slideDown(this.slide_speed);
+ return this;
+ },
+ hide: function () {
+ this.$el.slideUp({
+ duration: this.slide_speed
+ });
+ setTimeout(_.bind(SystemFeedbackView.prototype.hide, this, arguments),
+ this.slideSpeed);
+ }
+ });
+
+ // create Alert.Warning, Alert.Confirmation, etc
+ var capitalCamel, intents;
+ capitalCamel = _.compose(str.capitalize, str.camelize);
+ intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
+ _.each(intents, function(intent) {
+ var subclass;
+ subclass = Alert.extend({
+ options: $.extend({}, Alert.prototype.options, {
+ intent: intent
+ })
+ });
+ Alert[capitalCamel(intent)] = subclass;
+ });
+
+ return Alert;
+ });
+}).call(this, define || RequireJS.define);
diff --git a/common/static/common/js/components/views/feedback_notification.js b/common/static/common/js/components/views/feedback_notification.js
new file mode 100644
index 0000000000..2fb74f691d
--- /dev/null
+++ b/common/static/common/js/components/views/feedback_notification.js
@@ -0,0 +1,34 @@
+;(function (define) {
+ 'use strict';
+ define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"],
+ function($, _, str, SystemFeedbackView) {
+ str = str || _.str;
+ var Notification = SystemFeedbackView.extend({
+ options: $.extend({}, SystemFeedbackView.prototype.options, {
+ type: "notification",
+ closeIcon: false
+ })
+ });
+
+ // create Notification.Warning, Notification.Confirmation, etc
+ var capitalCamel, intents;
+ capitalCamel = _.compose(str.capitalize, str.camelize);
+ intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
+ _.each(intents, function(intent) {
+ var subclass;
+ subclass = Notification.extend({
+ options: $.extend({}, Notification.prototype.options, {
+ intent: intent
+ })
+ });
+ Notification[capitalCamel(intent)] = subclass;
+ });
+
+ // set more sensible defaults for Notification.Mini views
+ var miniOptions = Notification.Mini.prototype.options;
+ miniOptions.minShown = 1250;
+ miniOptions.closeIcon = false;
+
+ return Notification;
+ });
+}).call(this, define || RequireJS.define);
diff --git a/common/static/common/js/components/views/feedback_prompt.js b/common/static/common/js/components/views/feedback_prompt.js
new file mode 100644
index 0000000000..04a32da9c4
--- /dev/null
+++ b/common/static/common/js/components/views/feedback_prompt.js
@@ -0,0 +1,40 @@
+;(function (define) {
+ 'use strict';
+ define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"],
+ function($, _, str, SystemFeedbackView) {
+ str = str || _.str;
+ var Prompt = SystemFeedbackView.extend({
+ options: $.extend({}, SystemFeedbackView.prototype.options, {
+ type: "prompt",
+ closeIcon: false,
+ icon: false
+ }),
+ render: function() {
+ if(!window.$body) { window.$body = $(document.body); }
+ if(this.options.shown) {
+ $body.addClass('prompt-is-shown');
+ } else {
+ $body.removeClass('prompt-is-shown');
+ }
+ // super() in Javascript has awkward syntax :(
+ return SystemFeedbackView.prototype.render.apply(this, arguments);
+ }
+ });
+
+ // create Prompt.Warning, Prompt.Confirmation, etc
+ var capitalCamel, intents;
+ capitalCamel = _.compose(str.capitalize, str.camelize);
+ intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
+ _.each(intents, function(intent) {
+ var subclass;
+ subclass = Prompt.extend({
+ options: $.extend({}, Prompt.prototype.options, {
+ intent: intent
+ })
+ });
+ Prompt[capitalCamel(intent)] = subclass;
+ });
+
+ return Prompt;
+ });
+}).call(this, define || RequireJS.define);
diff --git a/common/static/common/js/spec/components/feedback_spec.js b/common/static/common/js/spec/components/feedback_spec.js
new file mode 100644
index 0000000000..5ef3600204
--- /dev/null
+++ b/common/static/common/js/spec/components/feedback_spec.js
@@ -0,0 +1,336 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+
+ define(["jquery", "common/js/components/views/feedback", "common/js/components/views/feedback_notification", "common/js/components/views/feedback_alert", "common/js/components/views/feedback_prompt", "sinon"], function($, SystemFeedback, NotificationView, AlertView, PromptView, sinon) {
+ var tpl;
+ tpl = readFixtures('system-feedback.underscore');
+ beforeEach(function() {
+ setFixtures(sandbox({
+ id: "page-alert"
+ }));
+ appendSetFixtures(sandbox({
+ id: "page-notification"
+ }));
+ appendSetFixtures(sandbox({
+ id: "page-prompt"
+ }));
+ appendSetFixtures($("