Merge lp:~gary/juju-gui/fixConflictResolution into lp:juju-gui/experimental

Proposed by Gary Poster
Status: Merged
Merged at revision: 1029
Proposed branch: lp:~gary/juju-gui/fixConflictResolution
Merge into: lp:juju-gui/experimental
Diff against target: 217 lines (+107/-25)
3 files modified
app/views/databinding.js (+19/-17)
app/views/inspector.js (+2/-2)
test/test_databinding.js (+86/-6)
To merge this branch: bzr merge lp:~gary/juju-gui/fixConflictResolution
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+184916@code.launchpad.net

Description of the change

Fix conflict resolution

Prior to this branch, conflict resolution did not play nicely with our new functionality to show modified fields and to hide/reveal save controls as appropriate. Specifically, these were two problems.

* If you set a model value but choose the user's value, the user's value will not be seen as a modified value.
* If you resolve a conflict that changes the DOM to match the model, the save dialog should disappear but does not.

This branch addresses these issues entirely within the databinding code.

In addition, I added a test of basic conflict handling. I cleaned up _getBinding further from my most recent branch. I bound the "resolve" method with the known arguments to make client code simpler.

https://codereview.appspot.com/13373052/

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Reviewers: mp+184916_code.launchpad.net,

Message:
Please take a look.

Description:
Fix conflict resolution

Prior to this branch, conflict resolution did not play nicely with our
new functionality to show modified fields and to hide/reveal save
controls as appropriate. Specifically, these were two problems.

* If you set a model value but choose the user's value, the user's value
will not be seen as a modified value.
* If you resolve a conflict that changes the DOM to match the model, the
save dialog should disappear but does not.

This branch addresses these issues entirely within the databinding code.

In addition, I added a test of basic conflict handling. I cleaned up
_getBinding further from my most recent branch. I bound the "resolve"
method with the known arguments to make client code simpler.

To QA, make a service and then open the inspector to the constraints
tab. Change the "CPU cores" field. Then in the console run something
like this (assuming that you did not type "8" in the CPU cores field:

app.db.services.item(0).set('constraints', {'cpu-cores': 8})

Select the second value (8) in the conflict resolution UX. The field
should be marked as unmodified and the save dialog should disappear.

Now change the CPU cores field to 4. In the console, set it the cores
to 6:

app.db.services.item(0).set('constraints', {'cpu-cores': 6})

Select the first value (4) in the conflict resolution UX. The conflict
marker should disappear but it should be replaced with the "modified"
asterisk, and the save button should still be visible (though it is
called "Overwrite," which is still technically accurate).

There are other edge cases I'm still planning on addressing in upcoming
branches, but these two are working, so I'm proposing the branch.

https://code.launchpad.net/~gary/juju-gui/fixConflictResolution/+merge/184916

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/13373052/

Affected files (+109, -25 lines):
   A [revision details]
   M app/views/databinding.js
   M app/views/inspector.js
   M test/test_databinding.js

Revision history for this message
Jeff Pihach (hatch) wrote :

Love the new interactions!

LGTM QA OK
IE OK

https://codereview.appspot.com/13373052/diff/1/app/views/databinding.js
File app/views/databinding.js (right):

https://codereview.appspot.com/13373052/diff/1/app/views/databinding.js#newcode814
app/views/databinding.js:814: Y.bind(resolve, self, binding.target,
binding.viewlet.name),
Nice cleanup!

https://codereview.appspot.com/13373052/

Revision history for this message
Gary Poster (gary) wrote :

*** Submitted:

Fix conflict resolution

Prior to this branch, conflict resolution did not play nicely with our
new functionality to show modified fields and to hide/reveal save
controls as appropriate. Specifically, these were two problems.

* If you set a model value but choose the user's value, the user's value
will not be seen as a modified value.
* If you resolve a conflict that changes the DOM to match the model, the
save dialog should disappear but does not.

This branch addresses these issues entirely within the databinding code.

In addition, I added a test of basic conflict handling. I cleaned up
_getBinding further from my most recent branch. I bound the "resolve"
method with the known arguments to make client code simpler.

R=jeff.pihach
CC=
https://codereview.appspot.com/13373052

https://codereview.appspot.com/13373052/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/views/databinding.js'
2--- app/views/databinding.js 2013-09-11 02:09:14 +0000
3+++ app/views/databinding.js 2013-09-11 03:35:27 +0000
4@@ -664,22 +664,21 @@
5 Find the binding for the given key (binding name).
6
7 @method _getBinding
8- @param {String} key Binding key.
9 @param {Y.Node} node The binding's target node.
10 @return {Binding} Binding reference.
11 */
12- BindingEngine.prototype._getBinding = function(key, node) {
13+ BindingEngine.prototype._getBinding = function(node) {
14 var binding;
15- if ( // Find the binding for the key, and break when found.
16+ if ( // Find the binding for the node, and break when found.
17 !this._bindings.some(function(b) {
18- if (b.name === key && b.target === node) {
19+ if (b.target === node) {
20 binding = b;
21 return true;
22 }})) {
23 // We don't expect this to ever happen. We should only be asking for
24 // a key that has been bound. If this does fail, let's be loud about
25 // it ASAP so that we can fix it.
26- throw 'Programmer error: no binding found for ' + key;
27+ throw 'Programmer error: no binding found for node';
28 }
29 return binding;
30 };
31@@ -708,7 +707,7 @@
32 var key = node.getData('bind');
33 var nodeHandler = this.getNodeHandler(node.getDOMNode());
34 var model = viewlet.model;
35- var binding = this._getBinding(key, node);
36+ var binding = this._getBinding(node);
37 if (nodeHandler.eq(node, binding.get(model))) {
38 delete viewlet.changedValues[key];
39 } else {
40@@ -812,7 +811,8 @@
41 viewlet.unsyncedFields();
42 binding.viewlet.conflict(
43 binding.target, viewletModel, binding.viewlet.name,
44- Y.bind(resolve, self), binding);
45+ Y.bind(resolve, self, binding.target, binding.viewlet.name),
46+ binding);
47 }
48
49 var value = binding.get(viewletModel);
50@@ -863,17 +863,19 @@
51 @param {Any} value that the user has accepted to resolve with.
52 */
53 BindingEngine.prototype.resolve = function(node, viewletName, value) {
54- var key = node.getData('bind'),
55- viewlet = this._viewlets[viewletName];
56-
57- delete viewlet.changedValues[key];
58- var field = this.getNodeHandler(node.getDOMNode());
59- field.set.call(this, node, value);
60- // If there are no more changed values then tell the
61- // the viewlet to update accordingly
62- if (Object.keys(viewlet.changedValues).length === 0) {
63- viewlet.syncedFields();
64+ var nodeHandler = this.getNodeHandler(node.getDOMNode());
65+ if (!nodeHandler.eq(node, value)) {
66+ nodeHandler.set(node, value);
67 }
68+ // Case 1:
69+ // The user chose the node value. It is still modified, so let the
70+ // viewlet have a chance to reflect this again, using _nodeChanged.
71+ // Case 2:
72+ // The user chose the model value, or some other value. Let
73+ // _nodeChanged have a chance to update changedValues and possibly call
74+ // syncedFields.
75+ var viewlet = this._viewlets[viewletName];
76+ this._nodeChanged(node, viewlet);
77 };
78
79 /**
80
81=== modified file 'app/views/inspector.js'
82--- app/views/inspector.js 2013-09-11 00:26:32 +0000
83+++ app/views/inspector.js 2013-09-11 03:35:27 +0000
84@@ -1229,9 +1229,9 @@
85 resolver.addClass('hidden');
86
87 if (e.currentTarget.hasClass('conflicted-env')) {
88- resolve(node, viewletName, modelValue);
89+ resolve(modelValue);
90 } else {
91- resolve(node, viewletName, formValue);
92+ resolve(formValue);
93 }
94 }
95
96
97=== modified file 'test/test_databinding.js'
98--- test/test_databinding.js 2013-09-11 02:09:14 +0000
99+++ test/test_databinding.js 2013-09-11 03:35:27 +0000
100@@ -24,11 +24,21 @@
101 container = utils.makeContainer();
102 container.setHTML(input);
103 viewlet = Object.create({
104+ name: 'testViewlet',
105 container: container,
106 changedValues: {},
107 _eventHandles: [],
108 syncedFields: function() {
109 this.calledSyncedFields = true;
110+ },
111+ unsyncedFields: function() {
112+ this.calledUnsyncedFields = true;
113+ },
114+ conflict: function() {
115+ this.conflictArgs = Array.prototype.slice.call(arguments);
116+ },
117+ changed: function() {
118+ this.changedArgs = Array.prototype.slice.call(arguments);
119 }
120 });
121 engine = new BindingEngine({interval: 0});
122@@ -731,9 +741,9 @@
123 var nodeA1 = container.one('textarea[data-bind="a"]');
124 var nodeA2 = container.one('input[data-bind="a"]');
125 var nodeB = container.one('[data-bind="b"]');
126- var bindingA1 = engine._getBinding('a', nodeA1);
127- var bindingA2 = engine._getBinding('a', nodeA2);
128- var bindingB = engine._getBinding('b', nodeB);
129+ var bindingA1 = engine._getBinding(nodeA1);
130+ var bindingA2 = engine._getBinding(nodeA2);
131+ var bindingB = engine._getBinding(nodeB);
132 assert.equal(bindingA1.name, 'a');
133 assert.strictEqual(bindingA1.target, nodeA1);
134 assert.equal(bindingA2.name, 'a');
135@@ -743,10 +753,80 @@
136 });
137
138 it('should throw an error when a binding is not found', function() {
139- var nodeA1 = container.one('textarea[data-bind="a"]');
140 assert.throws(
141- function() {engine._getBinding('c', nodeA1);},
142- 'Programmer error: no binding found for c');
143+ function() {engine._getBinding(container);},
144+ 'Programmer error: no binding found for node');
145+ });
146+ });
147+
148+ describe('_updateDOM tests', function() {
149+ var model, node;
150+
151+ beforeEach(function() {
152+ model = new Y.Model({a: undefined});
153+ generateEngine(
154+ '<textarea data-bind="a"></textarea>');
155+ engine.bind(model, viewlet);
156+ node = container.one('[data-bind="a"]');
157+ });
158+
159+ it('reports conflicts', function() {
160+ node.set('value', 'kumquat');
161+ engine._nodeChanged(node, viewlet);
162+ assert.deepEqual(viewlet.changedValues, {a: true});
163+ assert.isUndefined(viewlet.conflictArgs);
164+ model.set('a', 'rutebega');
165+ // We have a conflict!
166+ assert.isTrue(viewlet.calledUnsyncedFields);
167+ assert.isDefined(viewlet.conflictArgs);
168+ assert.strictEqual(viewlet.conflictArgs[0], node);
169+ assert.strictEqual(viewlet.conflictArgs[1], model);
170+ assert.strictEqual(viewlet.conflictArgs[2], 'testViewlet');
171+ // argument 3 should be the bound resolve function.
172+ // We'll assert this by showing that it works.
173+ viewlet.conflictArgs[3]('rutebega');
174+ assert.equal(node.get('value'), 'rutebega');
175+ assert.strictEqual(viewlet.conflictArgs[4],
176+ engine._getBinding(node));
177+ });
178+
179+ });
180+
181+ describe('resolve tests', function() {
182+ var model, node;
183+
184+ beforeEach(function() {
185+ model = new Y.Model({a: undefined});
186+ generateEngine(
187+ '<textarea data-bind="a"></textarea>');
188+ engine.bind(model, viewlet);
189+ node = container.one('[data-bind="a"]');
190+ node.set('value', 'kumquat');
191+ engine._nodeChanged(node, viewlet);
192+ model.set('a', 'rutebega');
193+ // Now we have a conflict.
194+ delete viewlet.changedArgs;
195+ });
196+
197+ afterEach(function() {
198+ container.remove().destroy(true);
199+ model.destroy(true);
200+ });
201+
202+ it('resolves to model value', function() {
203+ engine.resolve(node, viewlet.name, 'rutebega');
204+ assert.equal(node.get('value'), 'rutebega');
205+ assert.deepEqual(viewlet.changedValues, {});
206+ assert.isDefined(viewlet.changedArgs);
207+ assert.isTrue(viewlet.calledSyncedFields);
208+ });
209+
210+ it('resolves to input value', function() {
211+ engine.resolve(node, viewlet.name, 'kumquat');
212+ assert.equal(node.get('value'), 'kumquat');
213+ assert.deepEqual(viewlet.changedValues, {a: true});
214+ assert.isDefined(viewlet.changedArgs);
215+ assert.isUndefined(viewlet.calledSyncedFields);
216 });
217 });
218

Subscribers

People subscribed via source and target branches