Merge lp:~benji/juju-gui/charm-dnd-2 into lp:juju-gui/experimental

Proposed by Benji York
Status: Merged
Merged at revision: 765
Proposed branch: lp:~benji/juju-gui/charm-dnd-2
Merge into: lp:juju-gui/experimental
Diff against target: 660 lines (+327/-40) (has conflicts)
11 files modified
app/app.js (+6/-0)
app/store/env/fakebackend.js (+31/-24)
app/subapps/browser/views/editorial.js (+2/-1)
app/views/charm-panel.js (+1/-1)
app/views/topology/importexport.js (+8/-1)
app/views/topology/service.js (+21/-1)
app/widgets/charm-token.js (+82/-3)
test/index.html (+4/-0)
test/test_charm_token_drag_and_drop.js (+147/-0)
test/test_notifier_widget.js (+9/-9)
test/test_service_module.js (+16/-0)
Text conflict in test/index.html
To merge this branch: bzr merge lp:~benji/juju-gui/charm-dnd-2
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+171084@code.launchpad.net

Description of the change

Make charms able to be dragged to the environment.

https://codereview.appspot.com/10500044/

To post a comment you must log in.
lp:~benji/juju-gui/charm-dnd-2 updated
745. By Benji York

remove unneeded dependencies

746. By Benji York

checkpoint

747. By Benji York

tests

748. By Benji York

pre-review fixes

749. By Benji York

more pre-review fixes

Revision history for this message
Jeff Pihach (hatch) wrote :
Download full text (6.4 KiB)

This is such a cool feature thanks for implementing it. I added a
comment about deploying to the position that the user dropped as a
follow-up branch as it's probably a bit too much work to tack onto this
one :-)

My comments below are mostly just cleanup and few small improvement
requests. If you have any questions don't hesitate to ask!

https://codereview.appspot.com/10500044/diff/5001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/10500044/diff/5001/app/app.js#newcode582
app/app.js:582: Y.on('initiateDeploy', function(charm) {
as per comment in topology/service.js this should be:

this.views.environment.instance.topo.on('initiateDeploy', function() {
... });

Environment is a bubble target for the topology so it might be
preferable to do

this.env.on('initiateDeploy', ...
-or-
this.env.on('Topology:initiateDeploy', ...

I'd even entertain the idea of renaming the event to be
'deployFromCharm'

https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js#newcode387
app/views/topology/service.js:387: canvasDropHandler: function() {
- possible followup branch

This should take note of the drop position and place the service there
instead of the default deploy location.

https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js#newcode388
app/views/topology/service.js:388: // XXX Why do we have to dig so deep
just to get to the event? And why
Typically libraries will wrap native objects with their own convenience
layer and then provide access to the original one on a private property.
As for why it's not passed to the handler....not sure Makyo might know.

https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js#newcode395
app/views/topology/service.js:395: Y.fire('initiateDeploy', charm);
This should not be firing on Y, but firing from this class so that the
app can listen on this class for the event.

this.fire('initiateDeploy', charm);

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js
File app/widgets/charm-token.js (right):

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode112
app/widgets/charm-token.js:112: if (!container) {
should no longer be necessary as per my comments in the test script.

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode117
app/widgets/charm-token.js:117: this._makeDraggable(container,
container, charmData);
this._makeDraggable(container.one('a'), container, charmData) will get
you to a visibile UI while dragging without attaching to every child
element.

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode119
app/widgets/charm-token.js:119: container.all('*').each(function(child)
{
This can be removed as per comment above

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode133
app/widgets/charm-token.js:133: var outerContainer =
container.ancestor('.yui3-charmtoken')
this is the bounding box this.get('boundingBox').addClass('yui3-u');

Although I'...

Read more...

Revision history for this message
Gary Poster (gary) wrote :
Download full text (7.1 KiB)

On Jun 24, 2013, at 6:37 PM, Jeff Pihach <email address hidden> wrote:

> This is such a cool feature thanks for implementing it. I added a
> comment about deploying to the position that the user dropped as a
> follow-up branch as it's probably a bit too much work to tack onto this
> one :-)

Yes, that would be fine. If you do that, please create it as a high priority card in the inspector story.

Thank you,

Gary

>
> My comments below are mostly just cleanup and few small improvement
> requests. If you have any questions don't hesitate to ask!
>
>
> https://codereview.appspot.com/10500044/diff/5001/app/app.js
> File app/app.js (right):
>
> https://codereview.appspot.com/10500044/diff/5001/app/app.js#newcode582
> app/app.js:582: Y.on('initiateDeploy', function(charm) {
> as per comment in topology/service.js this should be:
>
> this.views.environment.instance.topo.on('initiateDeploy', function() {
> ... });
>
> Environment is a bubble target for the topology so it might be
> preferable to do
>
> this.env.on('initiateDeploy', ...
> -or-
> this.env.on('Topology:initiateDeploy', ...
>
> I'd even entertain the idea of renaming the event to be
> 'deployFromCharm'
>
> https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js
> File app/views/topology/service.js (right):
>
> https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js#newcode387
> app/views/topology/service.js:387: canvasDropHandler: function() {
> - possible followup branch
>
> This should take note of the drop position and place the service there
> instead of the default deploy location.
>
> https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js#newcode388
> app/views/topology/service.js:388: // XXX Why do we have to dig so deep
> just to get to the event? And why
> Typically libraries will wrap native objects with their own convenience
> layer and then provide access to the original one on a private property.
> As for why it's not passed to the handler....not sure Makyo might know.
>
> https://codereview.appspot.com/10500044/diff/5001/app/views/topology/service.js#newcode395
> app/views/topology/service.js:395: Y.fire('initiateDeploy', charm);
> This should not be firing on Y, but firing from this class so that the
> app can listen on this class for the event.
>
> this.fire('initiateDeploy', charm);
>
> https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js
> File app/widgets/charm-token.js (right):
>
> https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode112
> app/widgets/charm-token.js:112: if (!container) {
> should no longer be necessary as per my comments in the test script.
>
> https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode117
> app/widgets/charm-token.js:117: this._makeDraggable(container,
> container, charmData);
> this._makeDraggable(container.one('a'), container, charmData) will get
> you to a visibile UI while dragging without attaching to every child
> element.
>
> https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode119
> app/widgets/charm-token.js:119: c...

Read more...

Revision history for this message
Richard Harding (rharding) wrote :

Comments below. Jeff has some great feedback and I agree that there's
some work around the container/boundingBox that'll make things a bit
easier.

https://codereview.appspot.com/10500044/diff/1/app/widgets/charm-container.js
File app/widgets/charm-container.js (right):

https://codereview.appspot.com/10500044/diff/1/app/widgets/charm-container.js#newcode191
app/widgets/charm-container.js:191: _getDropZone: function() {
please make a valueFn so that it's done once and stored. It won't be
changing.

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js
File app/widgets/charm-token.js (right):

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode100
app/widgets/charm-token.js:100: element.on('dragstart',
Should we be catching this event to clean up later? This class is using
the Y.Event.EventTracker and you can do a
this.addEvent(element.on('dragstart...

Then it'll auto get removed and cleaned up on the widgets destroy.

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode129
app/widgets/charm-token.js:129: renderUI: function(container) {
I'm not sure why we need the container here. If you pass the boundingBox
into the widget cfg on init you'll have the correct space for the
content box and such.

https://codereview.appspot.com/10500044/diff/5001/app/widgets/charm-token.js#newcode135
app/widgets/charm-token.js:135: this._addDraggability(outerContainer);
This should only be run if the charm-token is instructed to be
draggable. This widget is shared among fullscreen, sidebar, and the
related charms side data uses. It doesn't make sense to have them
draggable in all cases. An ATTR for isDraggable should be added and then
set from the sidebar related views I'd think.

https://codereview.appspot.com/10500044/

lp:~benji/juju-gui/charm-dnd-2 updated
750. By Benji York

review changes

751. By Benji York

lint fixes

Revision history for this message
Richard Harding (rharding) wrote :

LGTM with the discussion on using boundingBox via cfg vs the container
and the simpler remove below.

https://codereview.appspot.com/10500044/diff/18001/test/test_charm_token_drag_and_drop.js
File test/test_charm_token_drag_and_drop.js (right):

https://codereview.appspot.com/10500044/diff/18001/test/test_charm_token_drag_and_drop.js#newcode44
test/test_charm_token_drag_and_drop.js:44:
outerContainer.remove().destroy(true);
you can just remove(true) and get it destroyed fully.

https://codereview.appspot.com/10500044/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks for this, going to be a really nice feature when it lands. I
think you need to merge trunk and maybe look at this again, the ghost
handling and the inspector stuff changes and possibly simplifies some of
this.

https://codereview.appspot.com/10500044/diff/18001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/10500044/diff/18001/app/app.js#newcode584
app/app.js:584: }, this.charmPanel);
We can't invoke the function above in cfg.deploy? we already switch the
impl based on the feature flag.

https://codereview.appspot.com/10500044/diff/18001/app/store/env/fakebackend.js
File app/store/env/fakebackend.js (right):

https://codereview.appspot.com/10500044/diff/18001/app/store/env/fakebackend.js#newcode1232
app/store/env/fakebackend.js:1232: var importImpl;
This looks like a story for another day.. Not sure what you must have
hit here to need this. :)

https://codereview.appspot.com/10500044/diff/18001/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/10500044/diff/18001/app/views/topology/service.js#newcode390
app/views/topology/service.js:390: // is it not just passed into the
handler?
I blame YUI :)

https://codereview.appspot.com/10500044/diff/18001/app/views/topology/service.js#newcode394
app/views/topology/service.js:394: var charmData =
Y.JSON.parse(evt.dataTransfer.getData('charmData'));
Why JSON parse at all if its a single string?

https://codereview.appspot.com/10500044/diff/18001/app/views/topology/service.js#newcode396
app/views/topology/service.js:396: Y.fire('initiateDeploy', charm);
If you merge trunk we also have db.services.ghostDeploy(charm) which
creates the ghost model from the charm. The rest of the system has been
changed to react to that, render the proper model, set up the inspector,
etc.

https://codereview.appspot.com/10500044/diff/18001/app/widgets/charm-token.js
File app/widgets/charm-token.js (right):

https://codereview.appspot.com/10500044/diff/18001/app/widgets/charm-token.js#newcode84
app/widgets/charm-token.js:84:
evt.dataTransfer.setDragImage(dragImage._node, 0, 0);
cute with the image

https://codereview.appspot.com/10500044/diff/18001/app/widgets/charm-token.js#newcode112
app/widgets/charm-token.js:112: var charmData =
Y.JSON.stringify(this.charmAttributes);
You only need the id here, no? If thats true then no need for JSON

https://codereview.appspot.com/10500044/diff/18001/test/test_notifier_widget.js
File test/test_notifier_widget.js (right):

https://codereview.appspot.com/10500044/diff/18001/test/test_notifier_widget.js#newcode97
test/test_notifier_widget.js:97: }, 500);
That must have been a hard one to track down.

https://codereview.appspot.com/10500044/

lp:~benji/juju-gui/charm-dnd-2 updated
752. By Benji York

review changes

753. By Benji York

fix lint

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/app.js'
2--- app/app.js 2013-06-25 19:27:16 +0000
3+++ app/app.js 2013-06-26 13:48:25 +0000
4@@ -593,6 +593,12 @@
5 this.on_database_changed, this);
6 }
7 this.addSubApplications(cfg);
8+
9+ // When someone wants a charm to be deployed they fire initiateDeploy
10+ // and we show the charm panel to configure/deploy the service.
11+ Y.on('initiateDeploy', function(charm) {
12+ this.deploy(charm);
13+ }, this.charmPanel);
14 },
15
16 /**
17
18=== modified file 'app/store/env/fakebackend.js'
19--- app/store/env/fakebackend.js 2013-06-25 19:36:29 +0000
20+++ app/store/env/fakebackend.js 2013-06-26 13:48:25 +0000
21@@ -28,7 +28,8 @@
22 YUI.add('juju-env-fakebackend', function(Y) {
23
24 var models = Y.namespace('juju.models');
25- var UNAUTHENTICATEDERROR = {error: 'Please log in.'};
26+ var UNAUTHENTICATED_ERROR = {error: 'Please log in.'};
27+ var VALUE_ERROR = {error: 'Unparsable environment data.'};
28
29 /**
30 Loops through the charm endpoint data to determine whether we have a
31@@ -163,7 +164,7 @@
32 */
33 nextChanges: function() {
34 if (!this.get('authenticated')) {
35- return UNAUTHENTICATEDERROR;
36+ return UNAUTHENTICATED_ERROR;
37 }
38 var result;
39 if (Y.Object.isEmpty(this.changes.services) &&
40@@ -205,7 +206,7 @@
41 */
42 nextAnnotations: function() {
43 if (!this.get('authenticated')) {
44- return UNAUTHENTICATEDERROR;
45+ return UNAUTHENTICATED_ERROR;
46 }
47 var result;
48 if (Y.Object.isEmpty(this.annotations.services) &&
49@@ -293,7 +294,7 @@
50 */
51 deploy: function(charmId, callback, options) {
52 if (!this.get('authenticated')) {
53- return callback(UNAUTHENTICATEDERROR);
54+ return callback(UNAUTHENTICATED_ERROR);
55 }
56 if (!options) {
57 options = {};
58@@ -502,7 +503,7 @@
59 */
60 destroyService: function(serviceName) {
61 if (!this.get('authenticated')) {
62- return UNAUTHENTICATEDERROR;
63+ return UNAUTHENTICATED_ERROR;
64 }
65 var service = this.db.services.getById(serviceName);
66 if (!service) {
67@@ -539,7 +540,7 @@
68 */
69 getService: function(serviceName) {
70 if (!this.get('authenticated')) {
71- return UNAUTHENTICATEDERROR;
72+ return UNAUTHENTICATED_ERROR;
73 }
74 var service = this.db.services.getById(serviceName);
75 if (!service) {
76@@ -567,7 +568,7 @@
77 */
78 getCharm: function(charmName, callback) {
79 if (!this.get('authenticated')) {
80- return callback(UNAUTHENTICATEDERROR);
81+ return callback(UNAUTHENTICATED_ERROR);
82 }
83 var formatCharm = function(charm) {
84 // Simulate a delay in the charm loading for testing.
85@@ -597,7 +598,7 @@
86 */
87 addUnit: function(serviceName, numUnits) {
88 if (!this.get('authenticated')) {
89- return UNAUTHENTICATEDERROR;
90+ return UNAUTHENTICATED_ERROR;
91 }
92 if (Y.Lang.isUndefined(numUnits)) {
93 numUnits = 1;
94@@ -742,7 +743,7 @@
95 warning, error;
96
97 if (!this.get('authenticated')) {
98- return UNAUTHENTICATEDERROR;
99+ return UNAUTHENTICATED_ERROR;
100 }
101
102 if (service) {
103@@ -775,7 +776,7 @@
104 warning, error;
105
106 if (!this.get('authenticated')) {
107- return UNAUTHENTICATEDERROR;
108+ return UNAUTHENTICATED_ERROR;
109 }
110 if (service) {
111 if (service.get('exposed')) {
112@@ -805,7 +806,7 @@
113 */
114 addRelation: function(endpointA, endpointB) {
115 if (!this.get('authenticated')) {
116- return UNAUTHENTICATEDERROR;
117+ return UNAUTHENTICATED_ERROR;
118 }
119 if ((typeof endpointA !== 'string') ||
120 (typeof endpointB !== 'string')) {
121@@ -886,7 +887,7 @@
122 */
123 removeRelation: function(endpointA, endpointB) {
124 if (!this.get('authenticated')) {
125- return UNAUTHENTICATEDERROR;
126+ return UNAUTHENTICATED_ERROR;
127 }
128 if ((typeof endpointA !== 'string') ||
129 (typeof endpointB !== 'string')) {
130@@ -960,7 +961,7 @@
131 */
132 updateAnnotations: function(entityName, annotations) {
133 if (!this.get('authenticated')) {
134- return UNAUTHENTICATEDERROR;
135+ return UNAUTHENTICATED_ERROR;
136 }
137 var entity = this.db.resolveModelByName(entityName);
138 var existing;
139@@ -993,7 +994,7 @@
140 */
141 getAnnotations: function(entityName) {
142 if (!this.get('authenticated')) {
143- return UNAUTHENTICATEDERROR;
144+ return UNAUTHENTICATED_ERROR;
145 }
146 var entity = this.db.resolveModelByName(entityName);
147
148@@ -1015,7 +1016,7 @@
149 */
150 removeAnnotations: function(entityName, keys) {
151 if (!this.get('authenticated')) {
152- return UNAUTHENTICATEDERROR;
153+ return UNAUTHENTICATED_ERROR;
154 }
155 var entity = this.db.resolveModelByName(entityName);
156 var annotations;
157@@ -1058,7 +1059,7 @@
158 */
159 setConfig: function(serviceName, config) {
160 if (!this.get('authenticated')) {
161- return UNAUTHENTICATEDERROR;
162+ return UNAUTHENTICATED_ERROR;
163 }
164 var service = this.db.services.getById(serviceName);
165 if (!service) {
166@@ -1096,7 +1097,7 @@
167 var constraints = {};
168
169 if (!this.get('authenticated')) {
170- return UNAUTHENTICATEDERROR;
171+ return UNAUTHENTICATED_ERROR;
172 }
173 var service = this.db.services.getById(serviceName);
174 if (!service) {
175@@ -1138,7 +1139,7 @@
176 */
177 resolved: function(unitName, relationName) {
178 if (!this.get('authenticated')) {
179- return UNAUTHENTICATEDERROR;
180+ return UNAUTHENTICATED_ERROR;
181 }
182 var unit = this.db.units.getById(unitName);
183 if (!unit) {
184@@ -1217,7 +1218,7 @@
185 };
186
187 if (!this.get('authenticated')) {
188- return UNAUTHENTICATEDERROR;
189+ return UNAUTHENTICATED_ERROR;
190 }
191
192 serviceList.each(function(s) {
193@@ -1260,11 +1261,17 @@
194 */
195 importEnvironment: function(jsonData, callback) {
196 if (!this.get('authenticated')) {
197- return callback(UNAUTHENTICATEDERROR);
198- }
199- var data = JSON.parse(jsonData),
200- version = 0,
201- importImpl;
202+ return callback(UNAUTHENTICATED_ERROR);
203+ }
204+ var data;
205+ try {
206+ data = JSON.parse(jsonData);
207+ } catch (e) {
208+ console.log('error parsing environment data');
209+ return callback(VALUE_ERROR);
210+ }
211+ var version = 0;
212+ var importImpl;
213 // Dispatch to the correct version after inspecting the JSON data.
214 if (data.meta && data.meta.exportFormat) {
215 version = data.meta.exportFormat;
216
217=== modified file 'app/subapps/browser/views/editorial.js'
218--- app/subapps/browser/views/editorial.js 2013-06-18 13:08:42 +0000
219+++ app/subapps/browser/views/editorial.js 2013-06-26 13:48:25 +0000
220@@ -115,7 +115,8 @@
221
222 var containerCfg = {
223 additionalChildConfig: {
224- size: this.get('isFullscreen') ? 'large' : 'small'
225+ size: this.get('isFullscreen') ? 'large' : 'small',
226+ isDraggable: !this.get('isFullscreen')
227 }
228 };
229
230
231=== modified file 'app/views/charm-panel.js'
232--- app/views/charm-panel.js 2013-06-20 15:37:45 +0000
233+++ app/views/charm-panel.js 2013-06-26 13:48:25 +0000
234@@ -856,7 +856,7 @@
235 charmId: charm.get('id')
236 });
237 // Since we are showing the configure/deploy panel ex nihilo, we want the
238- // panel to disappear when the deploy completes or is candled, but just
239+ // panel to disappear when the deploy completes or is canceled, but just
240 // this once (i.e., they should work normally if the user opens the panel
241 // via a charm search).
242 panels.configuration.once('panelRemoved', hide);
243
244=== modified file 'app/views/topology/importexport.js'
245--- app/views/topology/importexport.js 2013-06-25 20:49:59 +0000
246+++ app/views/topology/importexport.js 2013-06-26 13:48:25 +0000
247@@ -98,7 +98,14 @@
248 reader.readAsText(file);
249 });
250 } else {
251- env.importEnvironment(evt._event.dataTransfer.getData('Text'));
252+ // If this is not a drop event generated by the application (all
253+ // of which have a dataType), then it was caused by dropping
254+ // something external to the app, in which case it might be an
255+ // environment configuration, so process it as such.
256+ var dataType = evt._event.dataTransfer.getData('dataType');
257+ if (dataType === undefined) {
258+ env.importEnvironment(evt._event.dataTransfer.getData('Text'));
259+ }
260 }
261 evt.preventDefault();
262 evt.stopPropagation();
263
264=== modified file 'app/views/topology/service.js'
265--- app/views/topology/service.js 2013-06-23 22:54:49 +0000
266+++ app/views/topology/service.js 2013-06-26 13:48:25 +0000
267@@ -77,7 +77,8 @@
268 mouseout: 'serviceStatusMouseOut'
269 },
270 '.zoom-plane': {
271- click: 'canvasClick'
272+ click: 'canvasClick',
273+ drop: 'canvasDropHandler'
274 },
275 // Menu/Controls
276 '.view-service': {
277@@ -378,6 +379,25 @@
278 },
279
280 /**
281+ * Handle deploying a services by dropping a charm onto the canvas.
282+ *
283+ * @method canvasDropHandler
284+ * @static
285+ * @return {undefined} Nothing.
286+ */
287+ canvasDropHandler: function() {
288+ var evt = d3.event._event; // So well hidden.
289+ var dataType = evt.dataTransfer.getData('dataType');
290+ if (dataType === 'charm-token-drag-and-drop') {
291+ // The charm data was JSON encoded because the dataTransfer mechanism
292+ // only allows for string values.
293+ var charmData = Y.JSON.parse(evt.dataTransfer.getData('charmData'));
294+ var charm = new models.Charm(charmData);
295+ Y.fire('initiateDeploy', charm);
296+ }
297+ },
298+
299+ /**
300 * Clear any stateful actions (menus, etc.) when a clearState event is
301 * received.
302 *
303
304=== modified file 'app/widgets/charm-token.js'
305--- app/widgets/charm-token.js 2013-06-14 13:14:27 +0000
306+++ app/widgets/charm-token.js 2013-06-26 13:48:25 +0000
307@@ -38,6 +38,18 @@
308 TEMPLATE: Y.namespace('juju.views').Templates['charm-token'],
309
310 /**
311+ * Default general initializer method.
312+ *
313+ * @method initializer
314+ * @param {Object} cfg the config for the widget.
315+ * @return {undefined} Nothing.
316+ */
317+ initializer: function(charmAttributes) {
318+ // This passed-in config is made up of charm attributes.
319+ this.charmAttributes = charmAttributes;
320+ },
321+
322+ /**
323 Setter for the boundingBox attribute
324
325 **Override vs YUI to prevent node id setting based on BrowserCharm**
326@@ -56,15 +68,71 @@
327 },
328
329 /**
330+ * Generate a function that records drag and drop data when a drag starts.
331+ *
332+ * @method _makeDragStartHandler
333+ * @param {Node} dragImage The node to show during the drag.
334+ * @param {String} charmData The JSON encoded charm attributes.
335+ * @return {undefined} Nothing.
336+ */
337+ _makeDragStartHandler: function(dragImage, charmData) {
338+ return function(evt) {
339+ evt = evt._event; // We want the real event.
340+ evt.dataTransfer.effectAllowed = 'copy';
341+ evt.dataTransfer.setData('charmData', charmData);
342+ evt.dataTransfer.setData('dataType', 'charm-token-drag-and-drop');
343+ evt.dataTransfer.setDragImage(dragImage._node, 0, 0);
344+ };
345+ },
346+
347+ /**
348+ * Make an element draggable.
349+ *
350+ * @method _makeDraggable
351+ * @param {Node} element The node which is to be made draggable.
352+ * @param {Node} dragImage The node which will be displayed during
353+ * dragging.
354+ * @param {String} charmData The JSON encoded charm attributes.
355+ * @return {undefined} Nothing.
356+ */
357+ _makeDraggable: function(element, dragImage, charmData) {
358+ element.setAttribute('draggable', 'true');
359+ this.addEvent(element.on('dragstart',
360+ this._makeDragStartHandler(dragImage, charmData)));
361+ },
362+
363+ /**
364+ * Make the charm token draggable.
365+ *
366+ * @method _addDraggability
367+ * @param {Node} container he node which contains the charm list.
368+ * @return {undefined} Nothing; side-effects only.
369+ */
370+ _addDraggability: function(container) {
371+ // Since the browser's dataTransfer mechanism only accepts string values
372+ // we have to JSON encode the charm data.
373+ var charmData = Y.JSON.stringify(this.charmAttributes);
374+ this._makeDraggable(container, container, charmData);
375+ this._makeDraggable(container.one('a'), container, charmData);
376+ },
377+
378+ /**
379 * Create the nodes required by this widget and attach them to the DOM.
380 *
381+ * @param {Node} container The contaner to render into. Mainly for
382+ * testing.
383 * @method renderUI
384 */
385- renderUI: function() {
386+ renderUI: function(container) {
387 var content = this.TEMPLATE(this.getAttrs());
388- var container = this.get('contentBox');
389+ // This way we can pass in a container for easier testing.
390+ container = container || this.get('contentBox');
391 container.setHTML(content);
392- container.ancestor('.yui3-charmtoken').addClass('yui3-u');
393+ var outerContainer = container.ancestor('.yui3-charmtoken')
394+ .addClass('yui3-u');
395+ if (this.get('isDraggable')) {
396+ this._addDraggability(outerContainer);
397+ }
398 }
399
400 }, {
401@@ -147,6 +215,17 @@
402 */
403 size: {
404 value: 'small'
405+ },
406+ /**
407+ * Should the charm token be draggable?
408+ *
409+ * @attribute isDraggable
410+ * @default true
411+ * @type {Boolean}
412+ *
413+ */
414+ isDraggable: {
415+ value: true
416 }
417 }
418 });
419
420=== modified file 'test/index.html'
421--- test/index.html 2013-06-20 22:15:49 +0000
422+++ test/index.html 2013-06-26 13:48:25 +0000
423@@ -60,8 +60,12 @@
424 <script src="test_charm_panel.js"></script>
425 <script src="test_charm_store.js"></script>
426 <script src="test_charm_token.js"></script>
427+<<<<<<< TREE
428
429 <!-- FIXME: tests flicker, add container. -->
430+=======
431+ <script src="test_charm_token_drag_and_drop.js"></script>
432+>>>>>>> MERGE-SOURCE
433 <script src="test_charm_view.js"></script>
434
435 <script src="test_console.js"></script>
436
437=== added file 'test/test_charm_token_drag_and_drop.js'
438--- test/test_charm_token_drag_and_drop.js 1970-01-01 00:00:00 +0000
439+++ test/test_charm_token_drag_and_drop.js 2013-06-26 13:48:25 +0000
440@@ -0,0 +1,147 @@
441+/*
442+This file is part of the Juju GUI, which lets users view and manage Juju
443+environments within a graphical interface (https://launchpad.net/juju-gui).
444+Copyright (C) 2012-2013 Canonical Ltd.
445+
446+This program is free software: you can redistribute it and/or modify it under
447+the terms of the GNU Affero General Public License version 3, as published by
448+the Free Software Foundation.
449+
450+This program is distributed in the hope that it will be useful, but WITHOUT
451+ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
452+SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
453+General Public License for more details.
454+
455+You should have received a copy of the GNU Affero General Public License along
456+with this program. If not, see <http://www.gnu.org/licenses/>.
457+*/
458+
459+'use strict';
460+
461+describe('charm token drag and drop', function() {
462+ var Y, container, outerContainer, CharmToken, token;
463+
464+ before(function(done) {
465+ Y = YUI(GlobalConfig).use([
466+ 'browser-charm-token',
467+ 'juju-tests-utils'
468+ ],
469+ function(Y) {
470+ CharmToken = Y.juju.widgets.browser.CharmToken;
471+ done();
472+ });
473+
474+ });
475+
476+ beforeEach(function() {
477+ outerContainer = Y.namespace('juju-tests.utils').makeContainer()
478+ .addClass('yui3-charmtoken');
479+ container = Y.namespace('juju-tests.utils').makeContainer();
480+ outerContainer.append(container);
481+ });
482+
483+ afterEach(function() {
484+ outerContainer.remove().destroy(true);
485+ if (token) {
486+ token.destroy();
487+ }
488+ });
489+
490+ it('makes each charm token draggable', function() {
491+ var cfg = {
492+ id: 'test',
493+ name: 'some-charm',
494+ description: 'some description',
495+ recent_commit_count: 1,
496+ recent_download_count: 3,
497+ tested_providers: ['ec2']
498+ };
499+ token = new CharmToken(cfg);
500+ var draggable = [];
501+ token._makeDraggable = function(element, dragImage, charmData) {
502+ draggable.push(Y.JSON.parse(charmData).id);
503+ };
504+ token.renderUI(container);
505+ // Many elements are made draggable for a single token token.
506+ assert.isTrue(draggable.length > 1);
507+ // There is only a single token represented by all the draggable elements.
508+ draggable = Y.Array.dedupe(draggable);
509+ assert.equal(draggable.length, 1);
510+ // All of the charm tokens are made draggable.
511+ assert.deepEqual(draggable, ['test']);
512+ });
513+
514+ it('can make an element draggable', function() {
515+ token = new CharmToken();
516+ var setAttributeCalled, onCalled;
517+ var element = {
518+ setAttribute: function(name, value) {
519+ assert.equal(name, 'draggable');
520+ assert.equal(value, 'true');
521+ setAttributeCalled = true;
522+ },
523+ on: function(when, callable) {
524+ assert.equal(when, 'dragstart');
525+ onCalled = true;
526+ return {detach: function() {}};
527+ }
528+ };
529+ var dragImage = {};
530+ token._makeDragStartHandler = function(dragImage, charmData) {
531+ assert.equal(element, element);
532+ assert.equal(dragImage, dragImage);
533+ assert.equal(charmData, charmData);
534+ };
535+ token._makeDraggable(element, dragImage, 'charm-id');
536+ assert.isTrue(setAttributeCalled);
537+ assert.isTrue(onCalled);
538+ });
539+
540+ it('can set up drag and drop configuration', function() {
541+ token = new CharmToken();
542+ var setDataCalled, setDragImageCalled;
543+ var dragImage = {_node: {id: 'the real drag image'}};
544+ var charmData = 'data';
545+ var handler = token._makeDragStartHandler(dragImage, charmData);
546+ var dragDataSet = [];
547+ var evt = {
548+ _event: {
549+ dataTransfer: {
550+ setData: function(name, value) {
551+ dragDataSet.push([name, value]);
552+ setDataCalled = true;
553+ },
554+ setDragImage: function(provideDragImage, x, y) {
555+ assert.equal(provideDragImage, dragImage._node);
556+ assert.equal(x, 0);
557+ assert.equal(y, 0);
558+ setDragImageCalled = true;
559+ }
560+ }
561+ }
562+ };
563+ handler(evt);
564+ assert.equal(evt._event.dataTransfer.effectAllowed, 'copy');
565+ assert.equal(Y.JSON.stringify(dragDataSet),
566+ '[["charmData","data"],["dataType","charm-token-drag-and-drop"]]');
567+ assert.isTrue(setDataCalled);
568+ assert.isTrue(setDragImageCalled);
569+ });
570+
571+ it('respects the isDraggable switch', function() {
572+ token = new CharmToken({contentBox: container});
573+ token.set('isDraggable', false);
574+ var dragEnabled = false;
575+ token._addDraggability = function() {
576+ dragEnabled = true;
577+ };
578+ token.renderUI(container);
579+ // Since we set isDraggable to false, _addDraggability was not called.
580+ assert.isFalse(dragEnabled);
581+ // If we set isDraggable to true, _addDraggability will be called.
582+ token.set('isDraggable', true);
583+ token.renderUI(container);
584+ assert.isTrue(dragEnabled);
585+ });
586+
587+});
588
589=== modified file 'test/test_notifier_widget.js'
590--- test/test_notifier_widget.js 2013-05-17 14:51:05 +0000
591+++ test/test_notifier_widget.js 2013-06-26 13:48:25 +0000
592@@ -89,34 +89,34 @@
593
594 it('should destroy notifications after N milliseconds', function(done) {
595 makeNotifier('mytitle', 'mymessage', 1);
596- // A timeout of 250 milliseconds is used so that we ensure the destroying
597- // animation can be completed.
598+ // A timeout is used so that we ensure the destroying animation can be
599+ // completed.
600 setTimeout(function() {
601 assertNumNotifiers(0);
602 done();
603- }, 250);
604+ }, 500);
605 });
606
607 it('should destroy notifications on click', function(done) {
608 makeNotifier();
609 notifierBox.one('*').simulate('click');
610- // A timeout of 250 milliseconds is used so that we ensure the destroying
611- // animation can be completed.
612+ // A timeout is used so that we ensure the destroying animation can be
613+ // completed.
614 setTimeout(function() {
615 assertNumNotifiers(0);
616 done();
617- }, 250);
618+ }, 500);
619 });
620
621 it('should prevent notification removal on mouse enter', function(done) {
622 makeNotifier('mytitle', 'mymessage', 1);
623 notifierBox.one('*').simulate('mouseover');
624- // A timeout of 250 milliseconds is used so that we ensure the node is not
625- // preserved by the destroying animation.
626+ // A timeout is used so that we ensure the destroying animation can be
627+ // completed.
628 setTimeout(function() {
629 assertNumNotifiers(1);
630 done();
631- }, 250);
632+ }, 500);
633 });
634
635 });
636
637=== modified file 'test/test_service_module.js'
638--- test/test_service_module.js 2013-06-19 15:44:25 +0000
639+++ test/test_service_module.js 2013-06-26 13:48:25 +0000
640@@ -369,4 +369,20 @@
641 assert.deepPropertyVal(boxes, 'wordpress.model', wordpress);
642 });
643
644+ it('should deploy a service on charm token drop events', function(done) {
645+ d3.event._event = {dataTransfer: {
646+ getData: function(name) {
647+ if (name === 'dataType') {
648+ return 'charm-token-drag-and-drop';
649+ } else if (name === 'charmData') {
650+ return '{"id": "cs:foo/bar-1"}';
651+ }
652+ }}};
653+ var eventHandle = Y.on('initiateDeploy', function(charm) {
654+ eventHandle.detach();
655+ done();
656+ });
657+ serviceModule.canvasDropHandler();
658+ });
659+
660 });

Subscribers

People subscribed via source and target branches