launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06600
[Merge] lp:~rvb/maas/maas-bug-941751-js into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/maas-bug-941751-js into lp:maas with lp:~rvb/maas/maas-bug-941751 as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/maas-bug-941751-js/+merge/96112
This branch adds a Widget to maas.utils to be able to edit the MaaS' title "in place". I choose not to put it in the homepage js because we might want to reuse that widget and also I figured the dashboard js module will soon be crowded. The only trick is that we want the suffix " MaaS" to be appended to the title: the javascript has to take care of that when the user starts/stop to edit the title.
I had to introduce flash_{red, green} utilities to signal the success/failure of the background save.
--
https://code.launchpad.net/~rvb/maas/maas-bug-941751-js/+merge/96112
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-bug-941751-js into lp:maas.
=== modified file 'src/maasserver/static/js/testing/testing.js'
--- src/maasserver/static/js/testing/testing.js 2012-02-15 19:27:11 +0000
+++ src/maasserver/static/js/testing/testing.js 2012-03-06 12:35:34 +0000
@@ -29,8 +29,7 @@
/**
* Mock the '_io' field of the provided module. This assumes that
- * the module has a internal reference to its io module named '_io'
- * and that all its io is done via module._io.io(...).
+ * the module has a internal reference to its io module named '_io'.
*
* @method mockIO
* @param mock the mock object that should replace the module's io
@@ -43,6 +42,26 @@
},
/**
+ * Mock the '_io' field of the provided module with a silent method that
+ * simply records the call to 'send'. Whether or not 'send' was called
+ * will be stored in 'self.fired'.
+ * This assumes that the module has a internal reference to its io module
+ * named '_io' and that all its io is done via module._io.send(...).
+ *
+ * @method mockIO
+ * @param module the module to monkey patch
+ */
+ mockIOFired: function(module) {
+ this.fired = false;
+ var mockXhr = new Y.Base();
+ var self = this;
+ mockXhr.send = function(url, cfg) {
+ self.fired = true;
+ };
+ this.mockIO(mockXhr, module);
+ },
+
+ /**
* Register a method to be fired when the event 'name' is triggered on
* 'source'. The handle will be cleaned up when the test finishes.
*
=== modified file 'src/maasserver/static/js/tests/test_utils.html'
--- src/maasserver/static/js/tests/test_utils.html 2012-02-27 10:20:20 +0000
+++ src/maasserver/static/js/tests/test_utils.html 2012-03-06 12:35:34 +0000
@@ -4,16 +4,30 @@
<title>Test maas.utils</title>
<!-- YUI and test setup -->
- <script type="text/javascript" src="../yui/tests/yui/yui-min.js"></script>
+ <script type="text/javascript" src="../yui/tests/yui/yui.js"></script>
<script type="text/javascript" src="../testing/testrunner.js"></script>
<script type="text/javascript" src="../testing/testing.js"></script>
<!-- The module under test -->
<script type="text/javascript" src="../utils.js"></script>
<!-- The test suite -->
<script type="text/javascript" src="test_utils.js"></script>
+ <script type="text/javascript">
+ <!--
+ var MaaS_config = {
+ uris: {
+ maas_handler: '/api/maas/'
+ }
+ };
+ // -->
+ </script>
</head>
<body>
<span id="suite">maas.utils.tests</span>
+ <script type="text/x-template" id="title-form">
+ <form class="page-title-form">
+ <input type="text" value="Test Name MaaS"></input>
+ </form>
+ </script>
<div id="placeholder"></div>
</body>
</html>
=== modified file 'src/maasserver/static/js/tests/test_utils.js'
--- src/maasserver/static/js/tests/test_utils.js 2012-02-27 10:20:20 +0000
+++ src/maasserver/static/js/tests/test_utils.js 2012-03-06 12:35:34 +0000
@@ -10,13 +10,233 @@
var module = Y.maas.utils;
var suite = new Y.Test.Suite("maas.utils Tests");
-suite.add(new Y.maas.testing.TestCase({
- name: 'test-utils'
+var title_form = Y.one('#title-form').getContent();
+
+suite.add(new Y.maas.testing.TestCase({
+ name: 'test-utils-flash',
+
+ createNodeWithBaseColor: function(base_color) {
+ var node = Y.Node.create('<span />')
+ .set('text', "sample text");
+ node.setStyle('backgroundColor', base_color);
+ Y.one('#placeholder').empty().append(node);
+ return node;
+ },
+
+ assertAnimRestoresBackground: function(anim, node, base_color) {
+ var self = this;
+ anim.on("end", function(){
+ self.resume(function(){
+ Y.Assert.areEqual(
+ base_color, node.getStyle('backgroundColor'));
+ });
+ });
+ anim.run();
+ // Make sure we wait long enough (duration is in seconds, wait uses
+ // milliseconds (!)).
+ this.wait(module.FLASH_DURATION*1000*1.5);
+ },
+
+ test_base_flash_restores_background_color: function() {
+ var base_color = 'rgb(12, 11, 12)';
+ var from_color = 'rgb(122, 211, 142)';
+ var node = this.createNodeWithBaseColor(base_color);
+ var anim = module.base_flash(node, from_color);
+ this.assertAnimRestoresBackground(anim, node, base_color);
+ },
+
+ test_red_flash_restores_background_color: function() {
+ var base_color = 'rgb(12, 11, 12)';
+ var node = this.createNodeWithBaseColor(base_color);
+ var anim = module.red_flash(node);
+ this.assertAnimRestoresBackground(anim, node, base_color);
+ },
+
+ test_green_flash_restores_background_color: function() {
+ var base_color = 'rgb(12, 11, 12)';
+ var node = this.createNodeWithBaseColor(base_color);
+ var anim = module.green_flash(node);
+ this.assertAnimRestoresBackground(anim, node, base_color);
+ }
+
+}));
+
+
+suite.add(new Y.maas.testing.TestCase({
+ name: 'test-utils-titleeditwidget',
+
+ setUp: function() {
+ Y.one('#placeholder').empty().append(Y.Node.create(title_form));
+ },
+
+ createWidget: function() {
+ var widget = new module.TitleEditWidget({srcNode: '#placeholder'});
+ widget.render();
+ return widget;
+ },
+
+ test_getInput_returns_input: function() {
+ var widget = this.createWidget();
+ input = widget.get('srcNode').one('input');
+ Y.Assert.areEqual(input, widget.get('input'));
+ },
+
+ test_getTitle_returns_title: function() {
+ var widget = this.createWidget();
+ widget.get('srcNode').one('input').set('value', "Test value");
+ Y.Assert.areEqual("Test value", widget.get('title'));
+ },
+
+ test_setTitle_changes_title: function() {
+ var widget = this.createWidget();
+ widget.set('title', "Another value");
+ Y.Assert.areEqual(
+ "Another value",
+ widget.get('srcNode').one('input').get('value'));
+ },
+
+ test_hasSuffix_returns_true_if_suffix: function() {
+ var widget = this.createWidget();
+ widget.set('title', "Sample Title MaaS");
+ Y.Assert.isTrue(widget.hasSuffix());
+ },
+
+ test_hasSuffix_returns_false_if_not_suffix: function() {
+ var widget = this.createWidget();
+ widget.set('title', "Sample Title");
+ Y.Assert.isFalse(widget.hasSuffix());
+ },
+
+ test_removeSuffix_removes_suffix: function() {
+ var widget = this.createWidget();
+ widget.set('title', "Sample Title MaaS");
+ widget.removeSuffix();
+ Y.Assert.areEqual(
+ "Sample Title", widget.get('title'));
+ },
+
+ test_removeSuffix_doesnothing_if_suffix_not_present: function() {
+ var widget = this.createWidget();
+ widget.set('title', "Sample Title");
+ widget.removeSuffix();
+ Y.Assert.areEqual(
+ "Sample Title", widget.get('title'));
+ },
+
+ test_addSuffix_add_suffix: function() {
+ var widget = this.createWidget();
+ widget.set('title', "Sample Title");
+ widget.addSuffix();
+ Y.Assert.areEqual(
+ "Sample Title MaaS", widget.get('title'));
+ },
+
+ test_titleEditStart_starts_editing: function() {
+ var widget = this.createWidget();
+ widget._editing = false;
+ widget.set('title', "Sample Title MaaS");
+ widget.titleEditStart();
+ Y.Assert.isTrue(widget._editing);
+ Y.Assert.areEqual(
+ "Sample Title", widget.get('title'));
+ },
+
+ test_titleEditStart_does_nothing_if_already_editing: function() {
+ var widget = this.createWidget();
+ widget._editing = true;
+ widget.set('title', "Sample Title MaaS");
+ widget.titleEditStart();
+ Y.Assert.isTrue(widget._editing);
+ Y.Assert.areEqual(
+ "Sample Title MaaS", widget.get('title'));
+ },
+
+ test_titleEditEnd_stops_editing_if_currently_editing: function() {
+ var widget = this.createWidget();
+ widget._editing = true;
+ widget.set('title', "SampleTitle");
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.send = function(url, cfg) {
+ fired = true;
+ Y.Assert.areEqual(MaaS_config.uris.maas_handler, url);
+ Y.Assert.areEqual(
+ "op=set_config&name=maas_name&value=SampleTitle",
+ cfg.data);
+ };
+ this.mockIO(mockXhr, module);
+ widget.titleEditEnd();
+ Y.Assert.isTrue(fired);
+ Y.Assert.isFalse(widget._editing);
+ Y.Assert.areEqual(
+ "SampleTitle MaaS", widget.get('title'));
+ },
+
+ test_titleEditEnd_does_nothing_if_not_editing: function() {
+ var widget = this.createWidget();
+ widget._editing = false;
+ widget.set('title', "Sample Title");
+ this.mockIOFired(module);
+ widget.titleEditEnd();
+ Y.Assert.isFalse(this.fired);
+ Y.Assert.isFalse(widget._editing);
+ Y.Assert.areEqual(
+ "Sample Title", widget.get('title'));
+ },
+
+ test_input_click_starts_edition: function() {
+ var widget = this.createWidget();
+ widget._editing = false;
+ this.mockIOFired(module); // Silent io.
+ var input = widget.get('srcNode').one('input');
+ input.simulate('click');
+ Y.Assert.isTrue(widget._editing);
+ },
+
+ test_input_onchange_stops_editing: function() {
+ var widget = this.createWidget();
+ widget._editing = true;
+ this.mockIOFired(module); // Silent io.
+ var input = widget.get('srcNode').one('input');
+ input.simulate('change');
+ Y.Assert.isFalse(widget._editing);
+ },
+
+ test_input_enter_pressed_stops_editing: function() {
+ var widget = this.createWidget();
+ widget._editing = true;
+ this.mockIOFired(module); // Silent io.
+ var input = widget.get('srcNode').one('input');
+ // Simulate 'Enter' being pressed.
+ input.simulate("keypress", { keyCode: 13 });
+ Y.Assert.isFalse(widget._editing);
+ },
+
+ xtestDeleteTokenCall: function() {
+ // A click on the delete link calls the API to delete a token.
+ var mockXhr = new Y.Base();
+ var fired = false;
+ mockXhr.send = function(url, cfg) {
+ fired = true;
+ Y.Assert.areEqual(MaaS_config.uris.account_handler, url);
+ Y.Assert.areEqual(
+ "op=delete_authorisation_token&token_key=tokenkey1",
+ cfg.data);
+ };
+ this.mockIO(mockXhr, module);
+ var widget = this.createWidget();
+ this.addCleanup(function() { widget.destroy(); });
+ widget.render();
+ var link = widget.get('srcNode').one('.delete-link');
+ link.simulate('click');
+ Y.Assert.isTrue(fired);
+ }
+
}));
namespace.suite = suite;
}, '0.1', {'requires': [
- 'node', 'test', 'maas.testing', 'maas.utils']}
+ 'node', 'test', 'maas.testing', 'maas.utils', 'node-event-simulate']}
);
=== modified file 'src/maasserver/static/js/utils.js'
--- src/maasserver/static/js/utils.js 2012-02-27 10:20:20 +0000
+++ src/maasserver/static/js/utils.js 2012-03-06 12:35:34 +0000
@@ -11,5 +11,219 @@
Y.log('loading mass.utils');
var module = Y.namespace('maas.utils');
-}, '0.1', {'requires': ['base']}
+// Only used to mockup io in tests.
+module._io = new Y.IO();
+
+var FLASH_DURATION = 1;
+
+module.FLASH_DURATION = FLASH_DURATION;
+
+base_flash = function(obj, from_color) {
+ old_bg_color = Y.one(obj).getStyle('backgroundColor');
+
+ return new Y.Anim({
+ node: obj,
+ duration: FLASH_DURATION,
+ from: { backgroundColor: from_color},
+ to: { backgroundColor: old_bg_color}
+ });
+};
+
+module.base_flash = base_flash;
+
+/**
+ * @function green_flash
+ * @description A green flash and fade.
+ * @return Y.Anim instance
+ */
+var green_flash;
+
+green_flash = function(obj) {
+ return base_flash(obj, '#00FF00');
+};
+
+module.green_flash = green_flash;
+
+
+/**
+ * @function red_flash
+ * @description A red flash and fade, used to indicate errors.
+ * @return Y.Anim instance
+ */
+
+red_flash = function(obj) {
+ return base_flash(obj, '#FF0000');
+};
+
+module.red_flash = red_flash;
+
+
+var TitleEditWidget = function() {
+ TitleEditWidget.superclass.constructor.apply(this, arguments);
+};
+
+TitleEditWidget.NAME = 'title-edit-widget';
+
+TITLE_SUFFIX = ' MaaS';
+
+TitleEditWidget.ATTRS = {
+
+ /**
+ * MaaS's title input node.
+ *
+ * @attribute input
+ * @type Node
+ */
+ input: {
+ getter: function() {
+ return this.get('srcNode').one('input');
+ }
+ },
+
+ /**
+ * MaaS's title.
+ *
+ * @attribute title
+ * @type string
+ */
+ title: {
+ getter: function() {
+ return this.get('input').get('value');
+ },
+ setter: function(value) {
+ this.get('input').set('value', value);
+ }
+ }
+
+};
+
+
+Y.extend(TitleEditWidget, Y.Widget, {
+
+ initializer: function() {
+ // A boolean indicating whether or not the user is currently editing
+ // the title.
+ this._editing = false;
+ },
+
+ /**
+ * Does the input contain the suffix?
+ *
+ * @method hasSuffix
+ */
+ hasSuffix: function() {
+ var title = this.get('title');
+ var suffix = title.substring(
+ title.length - TITLE_SUFFIX.length, title.length);
+ return (suffix === TITLE_SUFFIX);
+ },
+
+ /**
+ * Add the suffix to the input's content.
+ *
+ * @method addSuffix
+ */
+ addSuffix: function() {
+ this.set('title', this.get('title') + TITLE_SUFFIX);
+ },
+
+ /**
+ * Remove the suffix to the input's content.
+ *
+ * @method removeSuffix
+ */
+ removeSuffix: function() {
+ if (this.hasSuffix()) {
+ var title = this.get('title');
+ var new_title = title.substring(
+ 0, title.length - TITLE_SUFFIX.length);
+ this.set('title', new_title);
+ }
+ else {
+ Y.log("Error: suffix not present in title.");
+ }
+ },
+
+ bindUI: function() {
+ var self = this;
+ var input = this.get('input');
+ // Click on the input node: start title edition.
+ input.on('click', function(e) {
+ e.preventDefault();
+ self.titleEditStart(e.rangeOffset);
+ });
+ // Change is fired when the input text as changed and the focus is now
+ // set another element.
+ input.on('change', function(e) {
+ e.preventDefault();
+ self.titleEditEnd();
+ });
+ // Form submitted (Enter pressed in the input Node).
+ this.get('srcNode').on('submit', function(e) {
+ e.preventDefault();
+ self.titleEditEnd();
+ });
+ },
+
+ /**
+ * Start of title edition: remove suffix and focus.
+ *
+ * @method titleEditStart
+ */
+ titleEditStart: function(rangeOffset) {
+ if (!this._editing) {
+ this._editing = true;
+ this.removeSuffix();
+ this.get('input').focus();
+ }
+ },
+
+ /**
+ * End of title edition: add suffix, persist the title and blur.
+ *
+ * @method titleEditEnd
+ */
+ titleEditEnd: function() {
+ if (this._editing) {
+ this.titlePersist();
+ this.addSuffix();
+ this._editing = false;
+ this.get('input').blur();
+ }
+ },
+
+ /**
+ * Call the API to make the title persist.
+ *
+ * @method titlePersist
+ */
+ titlePersist: function() {
+ var title = this.get('title');
+ var input = this.get('input');
+ var cfg = {
+ method: 'POST',
+ sync: false,
+ data: Y.QueryString.stringify({
+ op: 'set_config',
+ name: 'maas_name',
+ value: title
+ }),
+ on: {
+ success: function(id, out) {
+ green_flash(input).run();
+ },
+ failure: function(id, out) {
+ red_flash(input).run();
+ Y.log(out);
+ }
+ }
+ };
+ var request = module._io.send(
+ MaaS_config.uris.maas_handler, cfg);
+ }
+});
+
+module.TitleEditWidget = TitleEditWidget;
+
+}, '0.1', {'requires': ['base', 'widget', 'io', 'anim']}
);
=== modified file 'src/maasserver/templates/maasserver/index.html'
--- src/maasserver/templates/maasserver/index.html 2012-03-01 06:22:38 +0000
+++ src/maasserver/templates/maasserver/index.html 2012-03-06 12:35:34 +0000
@@ -10,7 +10,9 @@
{% block head %}
<script type="text/javascript">
<!--
- YUI().use('maas.node_add', 'maas.node','maas.node_views', function (Y) {
+ YUI().use(
+ 'maas.node_add', 'maas.node','maas.node_views', 'maas.utils',
+ function (Y) {
Y.on('load', function() {
// Create Dashboard view.
var view_container = Y.Node.create('<div />')
@@ -27,6 +29,12 @@
.addClass('button');
Y.one('#content').append(add_node_link);
add_node_link.on('click', Y.maas.node_add.showAddNodeWidget);
+
+ // Setup TitleEditWidget.
+ var title_widget = new Y.maas.utils.TitleEditWidget(
+ {srcNode: '.page-title-form'});
+ title_widget.render();
+
});
});
// -->
@@ -35,9 +43,9 @@
{% block page-title-block %}
{% if user.is_superuser %}
- {% comment %}TODO: wire this up so that it changes the MaaS title{% endcomment %}
<form action="" method="" class="page-title-form">
- <input type="text" value="{{ global_options.site_name }}" title="Edit the name of this MaaS" />
+ <input type="text" value="{{ global_options.site_name }} MaaS"
+ title="Edit the name of this MaaS" />
</form>
{% else %}
<h1 id="page-title">{% include "maasserver/site_title.html" %}</h1>
=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
--- src/maasserver/templates/maasserver/js-conf.html 2012-02-24 17:37:01 +0000
+++ src/maasserver/templates/maasserver/js-conf.html 2012-03-06 12:35:34 +0000
@@ -9,6 +9,7 @@
uris: {
login: '{% url "login" %}',
statics: '{{ STATIC_URL }}',
+ maas_handler: '{% url "maas_handler" %}',
nodes_handler: '{% url "nodes_handler" %}',
account_handler: '{% url "account_handler" %}'
},