Merge lp:~rharding/juju-gui/browser_links into lp:juju-gui/experimental
- browser_links
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 585 |
Proposed branch: | lp:~rharding/juju-gui/browser_links |
Merge into: | lp:juju-gui/experimental |
Diff against target: |
1110 lines (+717/-164) (has conflicts) 8 files modified
app/subapps/browser/browser.js (+323/-137) app/subapps/browser/templates/browser_charm.handlebars (+1/-1) app/subapps/browser/views/charm.js (+20/-1) app/subapps/browser/views/editorial.js (+57/-8) app/subapps/browser/views/view.js (+23/-1) app/templates/browser-search.handlebars (+2/-4) app/templates/charm-token.handlebars (+9/-0) test/test_browser_app.js (+282/-12) Text conflict in app/templates/charm-token.handlebars |
To merge this branch: | bzr merge lp:~rharding/juju-gui/browser_links |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+159215@code.launchpad.net |
Commit message
Description of the change
Support linking through router code for urls.
- Add state tracking to the subapp and use that to determine which UX parts
need calling and which do not.
- Add an event to the subapp and views to handle catching requests to route to
a new url.
- Add tests to walk the paths of rendering to make sure we're updating and
calling correctly.
- Add some stubs for search, but it's to be implemented in follow up.
Richard Harding (rharding) wrote : | # |
Richard Harding (rharding) wrote : | # |
Please take a look.
- 579. By Richard Harding
-
Garden
j.c.sackett (jcsackett) wrote : | # |
Rick, several comments below, and really I do think this probably needs
tests to land, since I'm seeing lots of places where the functionality
is pretty complicated.
Also, it seems like we might save ourselves some headache by removing
the "editorial is implicitly the default" notion, and simply having
"editorial" as a path component, and going explicit with everything.
That way we can simply check "is this the editorial view" rather than
checking several aspects of view state.
https:/
File app/subapps/
https:/
app/subapps/
Nitpicky, but your comment blocks seem to be misformatted. Shouldn't
there be stars as the leading character the whole way?
/**
* Some text
*/
https:/
app/subapps/
Y.merge(
Is this necessary? Can't you just do this._viewState = {...} and skip
merging with {}?
https:/
app/subapps/
I think this should just be 'charm' or 'charmDetails'; really, we're
just handling the routing here, not the rendering. Just as in fullscreen
and sidebar.
I just spent a bit of time lost, trying to figure out why we hadn't
decided *before* a render question if it was the right thing to do,
before realizing these methods *are* the methods deciding if a given
view is active/needs to be rendered.
https:/
app/subapps/
next) {
Same here.
https:/
app/subapps/
client.
These docs are misleading as well--we're not rendering, so much as
deciding if a given view needs to be active/rendered.
https:/
File app/subapps/
https:/
app/subapps/
class="back">{{#if isFullscreen}
We're going to be using some helper thing that generates the url here,
right?
https:/
File app/subapps/
https:/
app/subapps/
If container isn't a parameter anymore, we're not loading into a
specified container.
- 580. By Richard Harding
-
Pull in design work, trunk, update conflicts
- 581. By Richard Harding
-
Pull in sidebar ux adjustments
- 582. By Richard Harding
-
Tests are good
- 583. By Richard Harding
-
Add more tests to routing
- 584. By Richard Harding
-
Lint
Richard Harding (rharding) wrote : | # |
Please take a look.
Curtis Hovey (sinzui) wrote : | # |
Hi Rick. I have a few questions. Oh, and the remark about setStyle() is
a rant. I don't want this fixed this week.
https:/
File app/subapps/
https:/
app/subapps/
Why undefined? I expect undefined to be a state that has never been
examined/set. Why isn't null correct?
https:/
app/subapps/
Again, why define the value to be undefined? I think we mean the value
is null.
https:/
app/subapps/
Y.one('
I think setStyle('display', 'assumed-
done in several places. We should be adding/removing classes. While
'none' is valid for all html elements, 'block' is only valid for a
subset of elements. The design might need inline or inline-block in the
future to make content visible -- many code changes are needed where I
would prefer huw or myself to make a single style change to a class.
https:/
app/subapps/
content.
This is not an else. I can render search and editorial according to this
logic.
Richard Harding (rharding) wrote : | # |
Feedback below.
https:/
File app/subapps/
https:/
app/subapps/
On 2013/04/19 15:36:54, curtis wrote:
> Why undefined? I expect undefined to be a state that has never been
> examined/set. Why isn't null correct?
Null would completely work. I can update it.
https:/
app/subapps/
On 2013/04/19 15:36:54, curtis wrote:
> Again, why define the value to be undefined? I think we mean the value
is null.
Will change
https:/
app/subapps/
Y.one('
On 2013/04/19 15:36:54, curtis wrote:
> I think setStyle('display', 'assumed-
done in
> several places. We should be adding/removing classes. While 'none' is
valid for
> all html elements, 'block' is only valid for a subset of elements. The
design
> might need inline or inline-block in the future to make content
visible -- many
> code changes are needed where I would prefer huw or myself to make a
single
> style change to a class.
This is a bit of a temp hack since we're not the default, but out div
exists. I can XXX this to remove the whole thing going forward.
The other thing is that we have the collapsed view that's not here yet
that'll replace the job this is doing. I started to use the .hidden but
that uses visibility so our div is still there and 350px wide causing
click issues on the environment.
https:/
app/subapps/
content.
On 2013/04/19 15:36:54, curtis wrote:
> This is not an else. I can render search and editorial according to
this logic.
Thanks, missed this in refactors.
- 585. By Richard Harding
-
Fix pre review
Richard Harding (rharding) wrote : | # |
Please take a look.
Curtis Hovey (sinzui) wrote : | # |
Thank's Rick. LGTM.
Jeff Pihach (hatch) wrote : | # |
LGTM - There are a few rename and documentation requests below but good
work, thanks for working through this!
https:/
File app/subapps/
https:/
app/subapps/
These 4 methods which return true/false don't actually do what their
method names imply.
if (this._
assume that there are no side effects.
https:/
app/subapps/
Where does this element come from? Isn't it being rendered to the DOM
with the render() above?
https:/
app/subapps/
res, next) {
linter might complain about this not being documented with @method
https:/
app/subapps/
See above comments - this looks like it's showing the charm and then
checking for a truthy return value that it's succeeded
https:/
File app/subapps/
https:/
app/subapps/
could we document these attrs please.
https:/
File app/templates/
https:/
app/templates/
I think the indentation is off here.
https:/
File test/test_
https:/
test/test_
sidebar', function() {
what is this doing?
'renders details without re-rendering sidebar' ?
There are a few which aren't clear from their titles
- 586. By Richard Harding
-
Updates per code review
Richard Harding (rharding) wrote : | # |
*** Submitted:
Support linking through router code for urls.
- Add state tracking to the subapp and use that to determine which UX
parts
need calling and which do not.
- Add an event to the subapp and views to handle catching requests to
route to
a new url.
- Add tests to walk the paths of rendering to make sure we're updating
and
calling correctly.
- Add some stubs for search, but it's to be implemented in follow up.
R=j.c.sackett, curtis, jeff.pihach
CC=
https:/
Preview Diff
1 | === modified file 'app/subapps/browser/browser.js' |
2 | --- app/subapps/browser/browser.js 2013-04-17 22:57:48 +0000 |
3 | +++ app/subapps/browser/browser.js 2013-04-19 17:11:24 +0000 |
4 | @@ -21,45 +21,11 @@ |
5 | */ |
6 | ns.Browser = Y.Base.create('subapp-browser', Y.juju.SubApp, [], { |
7 | /** |
8 | - * Some routes might have sub parts that hint to where a user wants focus. |
9 | - * In particular we've got the tabs that might have focus. They are the |
10 | - * last optional component of some of the routes. |
11 | - * |
12 | - * @method _getSubPath |
13 | - * @param {String} path the full path to search for the sub path. |
14 | - * |
15 | - */ |
16 | - _getSubPath: function(path) { |
17 | - var reLastWord = /[^\/]*\/?$/, |
18 | - lastWords = path.match(reLastWord); |
19 | - if (lastWords.length) { |
20 | - return lastWords[0].replace('/', ''); |
21 | - } else { |
22 | - return undefined; |
23 | - } |
24 | - }, |
25 | - |
26 | - /** |
27 | - * Generate a standard shared set of cfg all Views can expect to see. |
28 | - * |
29 | - * @method _getViewCfg |
30 | - * @param {Object} cfg additional config to merge into the default view |
31 | - * config. |
32 | - * |
33 | - */ |
34 | - _getViewCfg: function(cfg) { |
35 | - return Y.merge(cfg, { |
36 | - db: this.get('db'), |
37 | - store: this.get('store') |
38 | - }); |
39 | - }, |
40 | - |
41 | - /** |
42 | - * Show or hide the details panel. |
43 | - * |
44 | - * @method _detailsVisible |
45 | - * @param {Boolean} visible set the panel to hide or show. |
46 | - * |
47 | + Show or hide the details panel. |
48 | + |
49 | + @method _detailsVisible |
50 | + @param {Boolean} visible set the panel to hide or show. |
51 | + |
52 | */ |
53 | _detailsVisible: function(visible) { |
54 | var detailsNode = Y.one('.bws-view-data'), |
55 | @@ -78,15 +44,183 @@ |
56 | }, |
57 | |
58 | /** |
59 | + Given the current subapp state, generate a url to pass up to the |
60 | + routing code to route to. |
61 | + |
62 | + @method _getStateUrl |
63 | + @param {Object} change the values to change in the current state. |
64 | + |
65 | + */ |
66 | + _getStateUrl: function(change) { |
67 | + var urlParts = ['/bws']; |
68 | + this._oldState = this._viewState; |
69 | + this._viewState = Y.merge(this._viewState, change); |
70 | + |
71 | + urlParts.push(this._viewState.viewmode); |
72 | + if (this._viewState.search) { |
73 | + urlParts.push('search'); |
74 | + } |
75 | + if (this._viewState.charmID) { |
76 | + urlParts.push(this._viewState.charmID); |
77 | + } |
78 | + |
79 | + // Always end on a / |
80 | + return urlParts.join('/'); |
81 | + }, |
82 | + |
83 | + /** |
84 | + * Generate a standard shared set of cfg all Views can expect to see. |
85 | + * |
86 | + * @method _getViewCfg |
87 | + * @param {Object} cfg additional config to merge into the default view |
88 | + * config. |
89 | + * |
90 | + */ |
91 | + _getViewCfg: function(cfg) { |
92 | + return Y.merge(cfg, { |
93 | + db: this.get('db'), |
94 | + store: this.get('store') |
95 | + }); |
96 | + }, |
97 | + |
98 | + /** |
99 | + Create an initial subapp state for later url generation. |
100 | + |
101 | + @method _initState |
102 | + |
103 | + */ |
104 | + _initState: function() { |
105 | + this._oldState = { |
106 | + viewmode: null, |
107 | + search: null, |
108 | + charmID: null |
109 | + }; |
110 | + this._viewState = Y.merge(this._oldState, {}); |
111 | + }, |
112 | + |
113 | + /** |
114 | + Determine if we should render the charm details based on the current |
115 | + state. |
116 | + |
117 | + @return {Boolean} true if should show. |
118 | + |
119 | + */ |
120 | + _shouldShowCharm: function() { |
121 | + if ( |
122 | + this._viewState.charmID && |
123 | + ( |
124 | + this._hasStateChanged('charmID') || |
125 | + this._hasStateChanged('viewmode') |
126 | + ) |
127 | + ) { |
128 | + return true; |
129 | + } else { |
130 | + return false; |
131 | + } |
132 | + }, |
133 | + |
134 | + /** |
135 | + Determine if we should render the editorial content based on the current |
136 | + state. |
137 | + |
138 | + @return {Boolean} true if should show. |
139 | + |
140 | + */ |
141 | + _shouldShowEditorial: function() { |
142 | + if ( |
143 | + !this._viewState.search && |
144 | + this._hasStateChanged('viewmode') |
145 | + ) { |
146 | + return true; |
147 | + } else { |
148 | + return false; |
149 | + } |
150 | + }, |
151 | + |
152 | + /** |
153 | + Determine if we should render the search results based on the current |
154 | + state. |
155 | + |
156 | + @return {Boolean} true if should show. |
157 | + |
158 | + */ |
159 | + _shouldShowSearch: function() { |
160 | + if ( |
161 | + this._viewState.search && |
162 | + ( |
163 | + this._hasStateChanged('search') || |
164 | + this._hasStateChanged('viewmode') || |
165 | + this._hasStateChanged('querystring') |
166 | + ) |
167 | + ) { |
168 | + return true; |
169 | + } else { |
170 | + return false; |
171 | + } |
172 | + }, |
173 | + |
174 | + /** |
175 | + Verify that a particular part of the state has changed. |
176 | + |
177 | + @method _hasStateChanged |
178 | + @param {String} field the part of the state to check. |
179 | + |
180 | + */ |
181 | + _hasStateChanged: function(field) { |
182 | + if (this._oldState[field] === this._viewState[field]) { |
183 | + return false; |
184 | + } else { |
185 | + return true; |
186 | + } |
187 | + }, |
188 | + |
189 | + /** |
190 | + Update the oldState with the viewState now that we're done processing |
191 | + the request. |
192 | + |
193 | + @method _saveState |
194 | + |
195 | + */ |
196 | + _saveState: function() { |
197 | + this._oldState = Y.merge( |
198 | + this._oldState, |
199 | + this._viewState); |
200 | + }, |
201 | + |
202 | + /** |
203 | + Given the params in the route determine what the new state is going to |
204 | + be. |
205 | + |
206 | + @method _updateState |
207 | + @param {String} path the requested path. |
208 | + @param {Object} params the params from the request payload. |
209 | + |
210 | + */ |
211 | + _updateState: function(path, params) { |
212 | + // Update the viewmode. Every request has a viewmode. |
213 | + this._viewState.viewmode = params.viewmode; |
214 | + |
215 | + // Check for a charm id in the request. |
216 | + if (params.id) { |
217 | + this._viewState.charmID = params.id; |
218 | + } else { |
219 | + this._viewState.charmID = null; |
220 | + } |
221 | + |
222 | + // Check for search in the request. |
223 | + if (path.indexOf('search') !== -1) { |
224 | + this._viewState.search = true; |
225 | + } else { |
226 | + this._viewState.search = false; |
227 | + } |
228 | + }, |
229 | + |
230 | + /** |
231 | * The available Views run from this sub app. |
232 | * @attribute views |
233 | * |
234 | */ |
235 | views: { |
236 | - charmDetails: { |
237 | - type: 'juju.browser.views.BrowserCharmView', |
238 | - preserve: false |
239 | - }, |
240 | fullscreen: { |
241 | type: 'juju.browser.views.FullScreen', |
242 | preserve: false |
243 | @@ -105,6 +239,7 @@ |
244 | */ |
245 | destructor: function() { |
246 | this._cacheCharms.destroy(); |
247 | + delete this._viewState; |
248 | }, |
249 | |
250 | /** |
251 | @@ -118,34 +253,53 @@ |
252 | // Hold onto charm data so we can pass model instances to other views when |
253 | // charms are selected. |
254 | this._cacheCharms = new models.BrowserCharmList(); |
255 | + this._initState(); |
256 | + |
257 | + // Listen for navigate events from any views we're rendering. |
258 | + this.on('*:viewNavigate', function(ev) { |
259 | + var url; |
260 | + if (ev.url) { |
261 | + url = ev.url; |
262 | + } else if (ev.change) { |
263 | + url = this._getStateUrl(ev.change); |
264 | + } |
265 | + this.navigate(url); |
266 | + }); |
267 | }, |
268 | |
269 | /** |
270 | - * Render the fullscreen view to the client. |
271 | + * Render the sidebar view of a specific charm to the client. |
272 | * |
273 | - * @method fullscreen |
274 | + * @method sidebarCharm |
275 | * @param {Request} req current request object. |
276 | * @param {Response} res current response object. |
277 | * @param {function} next callable for the next route in the chain. |
278 | * |
279 | */ |
280 | - fullscreen: function(req, res, next) { |
281 | - this.get('container').setStyle('display', 'block'); |
282 | - |
283 | - if (!this._fullscreen) { |
284 | - this._fullscreen = this.showView('fullscreen', this._getViewCfg(), { |
285 | - 'callback': function(view) { |
286 | - // if the fullscreen isn't the last part of the path, then ignore |
287 | - // the editorial content. |
288 | - if (this._getSubPath(req.path) === 'fullscreen') { |
289 | - this.renderEditorial(req, res, next); |
290 | - } |
291 | - next(); |
292 | - } |
293 | - }); |
294 | - } else { |
295 | - next(); |
296 | - } |
297 | + renderCharmDetails: function(req, res, next) { |
298 | + var charmID = req.params.id; |
299 | + var extraCfg = { |
300 | + charmID: charmID, |
301 | + container: Y.Node.create('<div class="charmview"/>') |
302 | + }; |
303 | + |
304 | + // The details view needs to know if we're using a fullscreen template |
305 | + // or the sidebar version. |
306 | + if (this._viewState.viewmode === 'fullscreen') { |
307 | + extraCfg.isFullscreen = true; |
308 | + } |
309 | + |
310 | + // Gotten from the sidebar creating the cache. |
311 | + var model = this._cacheCharms.getById(charmID); |
312 | + |
313 | + if (model) { |
314 | + extraCfg.charm = model; |
315 | + } |
316 | + |
317 | + this._details = new Y.juju.browser.views.BrowserCharmView( |
318 | + this._getViewCfg(extraCfg)); |
319 | + this._details.render(); |
320 | + this._details.addTarget(this); |
321 | }, |
322 | |
323 | /** |
324 | @@ -161,33 +315,79 @@ |
325 | |
326 | */ |
327 | renderEditorial: function(req, res, next) { |
328 | + // If loading the interesting content then it's not a search going on. |
329 | var container = this.get('container'), |
330 | editorialContainer, |
331 | extraCfg = {}; |
332 | |
333 | - if (req.path.indexOf('fullscreen') !== -1) { |
334 | + if (this._viewState.viewmode === 'fullscreen') { |
335 | // The fullscreen view requires that there be no editorial content if |
336 | // we're looking at a specific charm. The div we dump our content into |
337 | // is shared. So if the url is /fullscreen show editorial content, but |
338 | // if it's not, there's something else handling displaying the |
339 | // view-data. |
340 | - editorialContainer = container.one(' .bws-view-data'); |
341 | + extraCfg.renderTo = container.one('.bws-view-data'); |
342 | extraCfg.isFullscreen = true; |
343 | } else { |
344 | // If this is the sidebar view, then the editorial content goes into a |
345 | // different div since we can view both editorial content and |
346 | // view-data (such as a charm details) side by side. |
347 | - editorialContainer = container.one('.bws-content'); |
348 | - } |
349 | - |
350 | - if (!this._editorial) { |
351 | - this._editorial = new Y.juju.browser.views.EditorialView( |
352 | - this._getViewCfg(extraCfg)); |
353 | - |
354 | - this._editorial.render(editorialContainer); |
355 | - // Add any sidebar charms to the running cache. |
356 | - this._cacheCharms.add(this._editorial._cacheCharms); |
357 | - } |
358 | + extraCfg.renderTo = container.one('.bws-content'); |
359 | + } |
360 | + |
361 | + this._editorial = new Y.juju.browser.views.EditorialView( |
362 | + this._getViewCfg(extraCfg)); |
363 | + |
364 | + this._editorial.render(); |
365 | + this._editorial.addTarget(this); |
366 | + |
367 | + // Add any sidebar charms to the running cache. |
368 | + this._cacheCharms.add(this._editorial._cacheCharms); |
369 | + }, |
370 | + |
371 | + /** |
372 | + Place holder for a method to render out search so we can test url parsing |
373 | + |
374 | + */ |
375 | + renderSearchResults: function(req, res, next) { |
376 | + console.log('rendered search results.'); |
377 | + }, |
378 | + |
379 | + /** |
380 | + * Render the fullscreen view to the client. |
381 | + * |
382 | + * @method fullscreen |
383 | + * @param {Request} req current request object. |
384 | + * @param {Response} res current response object. |
385 | + * @param {function} next callable for the next route in the chain. |
386 | + * |
387 | + */ |
388 | + fullscreen: function(req, res, next) { |
389 | + // If we've switched to viewmode fullscreen, we need to render it. |
390 | + // We know the viewmode is already fullscreen because we're in this |
391 | + // function. |
392 | + if (this._hasStateChanged('viewmode')) { |
393 | + Y.one('#subapp-browser').setStyle('display', 'block'); |
394 | + this._fullscreen = this.showView('fullscreen', this._getViewCfg()); |
395 | + } |
396 | + |
397 | + // If we've changed the charmID or the viewmode has changed and we have |
398 | + // a charmID, render charmDetails. |
399 | + if (this._shouldShowCharm()) { |
400 | + this._detailsVisible(true); |
401 | + this.renderCharmDetails(req, res, next); |
402 | + } else if (this._shouldShowSearch()) { |
403 | + // Render search results if search is in the url and the viewmode or |
404 | + // the search has been changed in the state. |
405 | + this.renderSearchResults(req, res, next); |
406 | + } else { |
407 | + // Else render the editorial content. |
408 | + this.renderEditorial(req, res, next); |
409 | + } |
410 | + |
411 | + // Sync that the state has changed. |
412 | + this._saveState(); |
413 | + next(); |
414 | }, |
415 | |
416 | /** |
417 | @@ -200,72 +400,61 @@ |
418 | * |
419 | */ |
420 | sidebar: function(req, res, next) { |
421 | - this.get('container').setStyle('display', 'block'); |
422 | - // Clean up any details we've got. |
423 | - if (this._details) { |
424 | - this._details.destroy({remove: true}); |
425 | + // If we've switched to viewmode sidebar, we need to render it. |
426 | + if (this._hasStateChanged('viewmode')) { |
427 | + Y.one('#subapp-browser').setStyle('display', 'block'); |
428 | + this._sidebar = this.showView('sidebar', this._getViewCfg()); |
429 | + } |
430 | + |
431 | + // Render search results if search is in the url and the viewmode or the |
432 | + // search has been changed in the state. |
433 | + if (this._shouldShowSearch()) { |
434 | + this.renderSearchResults(req, res, next); |
435 | + } |
436 | + |
437 | + if (this._shouldShowEditorial()) { |
438 | + this.renderEditorial(req, res, next); |
439 | + } |
440 | + |
441 | + // If we've changed the charmID or the viewmode has changed and we have |
442 | + // a charmID, render charmDetails. |
443 | + if (this._shouldShowCharm()) { |
444 | + this._detailsVisible(true); |
445 | + this.renderCharmDetails(req, res, next); |
446 | } |
447 | |
448 | // If the sidebar is the final part of the route, then hide the div for |
449 | // viewing the charm details. |
450 | - if (this._getSubPath(req.path) === 'sidebar') { |
451 | + if (!this._viewState.charmID) { |
452 | this._detailsVisible(false); |
453 | + var detailsNode = Y.one('.bws-view-data'); |
454 | + if (detailsNode) { |
455 | + detailsNode.hide(); |
456 | + } |
457 | + // Clean up any details we've got. |
458 | + if (this._details) { |
459 | + this._details.destroy({remove: true}); |
460 | + } |
461 | } |
462 | |
463 | - if (!this._sidebar) { |
464 | - // Whenever the sidebar view is rendered it needs some editorial |
465 | - // content to display to the user. We only need once instance though, |
466 | - // so only render it on the first view. As users click on charm to |
467 | - // charm and we generate urls /sidebar/precise/xxx we don't want to |
468 | - // re-render the sidebar content. |
469 | - this._sidebar = this.showView('sidebar', this._getViewCfg(), { |
470 | - 'callback': function(view) { |
471 | - this.renderEditorial(req, res, next); |
472 | - next(); |
473 | - } |
474 | - }); |
475 | - } else { |
476 | - next(); |
477 | - } |
478 | + // Sync that the state has changed. |
479 | + this._saveState(); |
480 | + next(); |
481 | }, |
482 | |
483 | /** |
484 | - * Render the sidebar view of a specific charm to the client. |
485 | - * |
486 | - * @method sidebarCharm |
487 | - * @param {Request} req current request object. |
488 | - * @param {Response} res current response object. |
489 | - * @param {function} next callable for the next route in the chain. |
490 | - * |
491 | + Dispatch to the correct viewmode based on the route that was hit. |
492 | + |
493 | + @method routeView |
494 | + @param {Request} req current request object. |
495 | + @param {Response} res current response object. |
496 | + @param {function} next callable for the next route in the chain. |
497 | + |
498 | */ |
499 | - charmDetails: function(req, res, next) { |
500 | - var charmID = req.params.id; |
501 | - var extraCfg = { |
502 | - charmID: charmID, |
503 | - container: Y.Node.create('<div class="charmview"/>') |
504 | - }; |
505 | - |
506 | - // The details view needs to know if we're using a fullscreen template |
507 | - // or the sidebar version. |
508 | - if (req.path.indexOf('fullscreen') !== -1) { |
509 | - extraCfg.isFullscreen = true; |
510 | - } |
511 | - |
512 | - // Gotten from the sidebar creating the cache. |
513 | - var model = this._cacheCharms.getById(charmID); |
514 | - |
515 | - if (model) { |
516 | - extraCfg.charm = model; |
517 | - } |
518 | - |
519 | - this._details = new Y.juju.browser.views.BrowserCharmView( |
520 | - this._getViewCfg(extraCfg)); |
521 | - this._details.render(); |
522 | - // Make sure we show the bws-view-data div that the details renders |
523 | - // into. |
524 | - this._detailsVisible(true); |
525 | - |
526 | - next(); |
527 | + routeView: function(req, res, next) { |
528 | + // Update the state for the rest of things to figure out what to do. |
529 | + this._updateState(req.path, req.params); |
530 | + this[req.params.viewmode](req, res, next); |
531 | } |
532 | |
533 | }, { |
534 | @@ -318,13 +507,10 @@ |
535 | routes: { |
536 | value: [ |
537 | // Double routes are needed to catch /fullscreen and /fullscreen/ |
538 | - { path: '/bws/fullscreen/', callbacks: 'fullscreen' }, |
539 | - { path: '/bws/fullscreen/*/', callbacks: 'fullscreen' }, |
540 | - { path: '/bws/fullscreen/*id/', callbacks: 'charmDetails' }, |
541 | - |
542 | - { path: '/bws/sidebar/', callbacks: 'sidebar' }, |
543 | - { path: '/bws/sidebar/*/', callbacks: 'sidebar' }, |
544 | - { path: '/bws/sidebar/*id/', callbacks: 'charmDetails' } |
545 | + { path: '/bws/:viewmode/', callbacks: 'routeView' }, |
546 | + { path: '/bws/:viewmode/search/', callbacks: 'routeView' }, |
547 | + { path: '/bws/:viewmode/search/*id/', callbacks: 'routeView' }, |
548 | + { path: '/bws/:viewmode/*id/', callbacks: 'routeView' } |
549 | ] |
550 | }, |
551 | |
552 | |
553 | === modified file 'app/subapps/browser/templates/browser_charm.handlebars' |
554 | --- app/subapps/browser/templates/browser_charm.handlebars 2013-04-19 06:29:32 +0000 |
555 | +++ app/subapps/browser/templates/browser_charm.handlebars 2013-04-19 17:11:24 +0000 |
556 | @@ -1,7 +1,7 @@ |
557 | <div class="charm yui3-g"> |
558 | <div class="content yui3-u-{{#if isFullscreen}}5-6{{else}}1{{/if}}"> |
559 | <div class="nav"> |
560 | - <a href="/bws/sidebar/" class="back">{{#if isFullscreen}}Back{{else}}Close{{/if}}</a> |
561 | + <a href="" class="back">{{#if isFullscreen}}Back{{else}}Close{{/if}}</a> |
562 | <a href="" class="add">Add</a> |
563 | <a href="" class="share"> |
564 | <img src="/juju-ui/assets/images/share_icon.jpg" alt="Share" /> |
565 | |
566 | === modified file 'app/subapps/browser/views/charm.js' |
567 | --- app/subapps/browser/views/charm.js 2013-04-18 19:07:41 +0000 |
568 | +++ app/subapps/browser/views/charm.js 2013-04-19 17:11:24 +0000 |
569 | @@ -38,6 +38,9 @@ |
570 | }, |
571 | '#bws-hooks select': { |
572 | change: '_loadHookContent' |
573 | + }, |
574 | + '.nav .back': { |
575 | + click: '_handleBack' |
576 | } |
577 | }, |
578 | |
579 | @@ -194,6 +197,23 @@ |
580 | }, |
581 | |
582 | /** |
583 | + Handle the back button being clicked on from the header of the |
584 | + details. |
585 | + |
586 | + @method _handleBack |
587 | + @param {Event} ev the click event handler. |
588 | + |
589 | + */ |
590 | + _handleBack: function(ev) { |
591 | + ev.halt(); |
592 | + this.fire('viewNavigate', { |
593 | + change: { |
594 | + charmID: null |
595 | + } |
596 | + }); |
597 | + }, |
598 | + |
599 | + /** |
600 | * Determine which intro copy to display depending on the number |
601 | * of interfaces. |
602 | * |
603 | @@ -242,7 +262,6 @@ |
604 | build += string; |
605 | } |
606 | }); |
607 | - |
608 | interfaceIntro[build] = true; |
609 | return interfaceIntro; |
610 | }, |
611 | |
612 | === modified file 'app/subapps/browser/views/editorial.js' |
613 | --- app/subapps/browser/views/editorial.js 2013-04-17 01:09:21 +0000 |
614 | +++ app/subapps/browser/views/editorial.js 2013-04-19 17:11:24 +0000 |
615 | @@ -25,6 +25,31 @@ |
616 | ns.EditorialView = Y.Base.create('browser-view-sidebar', Y.View, [], { |
617 | template: views.Templates.editorial, |
618 | |
619 | + events: { |
620 | + '.charm-token': { |
621 | + click: '_handleCharmSelection' |
622 | + } |
623 | + }, |
624 | + |
625 | + /** |
626 | + When selecting a charm from the list make sure we re-route the app to |
627 | + the details view with that charm selected. |
628 | + |
629 | + @method _handleCharmSelection |
630 | + @param {Event} ev the click event handler for the charm selected. |
631 | + |
632 | + */ |
633 | + _handleCharmSelection: function(ev) { |
634 | + var charm = ev.currentTarget; |
635 | + var charmID = charm.getData('charmid'); |
636 | + |
637 | + this.fire('viewNavigate', { |
638 | + change: { |
639 | + charmID: charmID |
640 | + } |
641 | + }); |
642 | + }, |
643 | + |
644 | /** |
645 | * General YUI initializer. |
646 | * |
647 | @@ -45,17 +70,11 @@ |
648 | * @param {Node} container An optional node to override where it's going. |
649 | * |
650 | */ |
651 | - render: function(container) { |
652 | + render: function() { |
653 | var tpl = this.template(this.getAttrs()), |
654 | tplNode = Y.Node.create(tpl), |
655 | store = this.get('store'); |
656 | |
657 | - if (typeof container !== 'object') { |
658 | - container = this.get('container'); |
659 | - } else { |
660 | - this.set('container', container); |
661 | - } |
662 | - |
663 | // By default we grab the editorial content from the api to use for |
664 | // display. |
665 | this.get('store').interesting({ |
666 | @@ -96,7 +115,9 @@ |
667 | }); |
668 | newCharmContainer.render(newContainer); |
669 | |
670 | - container.setHTML(tplNode); |
671 | + var container = this.get('container'); |
672 | + container.append(tplNode); |
673 | + this.get('renderTo').setHTML(container); |
674 | |
675 | // Add the charms to the cache for use in other views. |
676 | // Start with a reset to empty any current cached models. |
677 | @@ -144,9 +165,37 @@ |
678 | } |
679 | }, { |
680 | ATTRS: { |
681 | + /** |
682 | + * Is this rendering of the editorial view for fullscreen or sidebar |
683 | + * purposes? |
684 | + * |
685 | + * @attribute isFullscreen |
686 | + * @default false |
687 | + * @type {Boolean} |
688 | + * |
689 | + */ |
690 | isFullscreen: { |
691 | value: false |
692 | }, |
693 | + /** |
694 | + * What is the container node we should render our container into? |
695 | + * |
696 | + * @attribute renderTo |
697 | + * @default undefined |
698 | + * @type {Node} |
699 | + * |
700 | + */ |
701 | + renderTo: { |
702 | + |
703 | + }, |
704 | + /** |
705 | + * The Charmworld0 Api store instance for loading content. |
706 | + * |
707 | + * @attribute store |
708 | + * @default undefined |
709 | + * @type {Charmworld0} |
710 | + * |
711 | + */ |
712 | store: { |
713 | |
714 | } |
715 | |
716 | === modified file 'app/subapps/browser/views/view.js' |
717 | --- app/subapps/browser/views/view.js 2013-04-12 19:53:11 +0000 |
718 | +++ app/subapps/browser/views/view.js 2013-04-19 17:11:24 +0000 |
719 | @@ -76,9 +76,13 @@ |
720 | this.search.on( |
721 | this.search.EVT_TOGGLE_VIEWABLE, this._toggleBrowser, this) |
722 | ); |
723 | + |
724 | + this.addEvent( |
725 | + this.search.on( |
726 | + this.search.EVT_TOGGLE_FULLSCREEN, this._toggleFullscreen, this) |
727 | + ); |
728 | }, |
729 | |
730 | - |
731 | /** |
732 | * Render out the main search widget and controls shared across various |
733 | * views. |
734 | @@ -116,6 +120,7 @@ |
735 | * |
736 | */ |
737 | _toggleBrowser: function(ev) { |
738 | + ev.halt(); |
739 | var sidebar = Y.one('.charmbrowser'); |
740 | |
741 | if (this.visible) { |
742 | @@ -127,6 +132,23 @@ |
743 | } |
744 | }, |
745 | |
746 | + /** |
747 | + Upon clicking the fullscreen toggle icon make sure we re-route to the |
748 | + new form of the UX. |
749 | + |
750 | + @method _toggleFullscreen |
751 | + @param {Event} ev the click event handler on the button. |
752 | + |
753 | + */ |
754 | + _toggleFullscreen: function(ev) { |
755 | + ev.halt(); |
756 | + var change = { |
757 | + viewmode: this.isFullscreen() ? 'sidebar' : 'fullscreen' |
758 | + }; |
759 | + this.fire('viewNavigate', { |
760 | + change: change |
761 | + }); |
762 | + }, |
763 | |
764 | /** |
765 | * Shared method to generate a message to the user based on a bad api |
766 | |
767 | === modified file 'app/templates/browser-search.handlebars' |
768 | --- app/templates/browser-search.handlebars 2013-04-05 13:10:59 +0000 |
769 | +++ app/templates/browser-search.handlebars 2013-04-19 17:11:24 +0000 |
770 | @@ -11,9 +11,7 @@ |
771 | </form> |
772 | </div> |
773 | <div class="toggle-fullscreen"> |
774 | - <a href="{{ fullscreenTarget }}"> |
775 | - <img src="/juju-ui/assets/images/browser_minimize.jpg" |
776 | - alt="Minimize" /> |
777 | - </a> |
778 | + <img src="/juju-ui/assets/images/browser_minimize.jpg" |
779 | + alt="Minimize" /> |
780 | </div> |
781 | </div> |
782 | |
783 | === modified file 'app/templates/charm-token.handlebars' |
784 | --- app/templates/charm-token.handlebars 2013-04-19 06:29:32 +0000 |
785 | +++ app/templates/charm-token.handlebars 2013-04-19 17:11:24 +0000 |
786 | @@ -1,12 +1,21 @@ |
787 | <a href="/bws/sidebar/{{id}}" |
788 | class="charm-token yui3-g" |
789 | data-charmid="{{id}}"> |
790 | +<<<<<<< TREE |
791 | <span class="yui3-u"> |
792 | {{#if iconfile }} |
793 | <img src="data:image/svg;base64,{{ iconfile }}" alt="{{ name }} icon"> |
794 | {{else}} |
795 | <div class="charm-icon"></div> |
796 | {{/if}} |
797 | +======= |
798 | + <span class="yui3-u"> |
799 | + {{#if iconfile }} |
800 | + <img src="data:image/svg;base64,{{ iconfile }}" alt="{{ name }} icon"> |
801 | + {{else}} |
802 | + <div class="charm-icon"></div> |
803 | + {{/if}} |
804 | +>>>>>>> MERGE-SOURCE |
805 | </span> |
806 | <span class="yui3-u-3-4"> |
807 | <span class="title"> |
808 | |
809 | === modified file 'test/test_browser_app.js' |
810 | --- test/test_browser_app.js 2013-04-15 16:19:55 +0000 |
811 | +++ test/test_browser_app.js 2013-04-19 17:11:24 +0000 |
812 | @@ -156,16 +156,286 @@ |
813 | }); |
814 | }); |
815 | |
816 | - it('should be able to determine if the route is a sub path', function() { |
817 | - var app = new browser.Browser(), |
818 | - subpaths = ['configuration', 'hooks', 'interfaces', 'qa', 'readme']; |
819 | - |
820 | - Y.Array.each(subpaths, function(path) { |
821 | - var url = '/bws/fullscreen/charm/id/stuff/' + path + '/'; |
822 | - app._getSubPath(url).should.eql(path); |
823 | - }); |
824 | - }); |
825 | - |
826 | - }); |
827 | - |
828 | + }); |
829 | + |
830 | + describe('browser subapp display tree', function() { |
831 | + var Y, browser, hits, ns, resetHits; |
832 | + |
833 | + before(function(done) { |
834 | + Y = YUI(GlobalConfig).use( |
835 | + 'app-subapp-extension', |
836 | + 'juju-views', |
837 | + 'juju-browser', |
838 | + 'subapp-browser', function(Y) { |
839 | + browser = Y.namespace('juju.subapps'); |
840 | + |
841 | + resetHits = function() { |
842 | + hits = { |
843 | + fullscreen: false, |
844 | + sidebar: false, |
845 | + renderCharmDetails: false, |
846 | + renderEditorial: false, |
847 | + renderSearchResults: false |
848 | + }; |
849 | + }; |
850 | + done(); |
851 | + }); |
852 | + }); |
853 | + |
854 | + before(function(done) { |
855 | + Y = YUI(GlobalConfig).use( |
856 | + 'juju-views', |
857 | + 'juju-browser', |
858 | + 'subapp-browser', function(Y) { |
859 | + ns = Y.namespace('juju.subapps'); |
860 | + done(); |
861 | + }); |
862 | + }); |
863 | + |
864 | + beforeEach(function() { |
865 | + var docBody = Y.one(document.body); |
866 | + Y.Node.create('<div id="subapp-browser">' + |
867 | + '</div>').appendTo(docBody); |
868 | + |
869 | + // Track which render functions are hit. |
870 | + resetHits(); |
871 | + |
872 | + // Mock out a dummy location for the Store used in view instances. |
873 | + window.juju_config = { |
874 | + charmworldURL: 'http://localhost' |
875 | + }; |
876 | + |
877 | + browser = new ns.Browser(); |
878 | + // Block out each render target so we only track it was hit. |
879 | + browser.renderCharmDetails = function() { |
880 | + hits.renderCharmDetails = true; |
881 | + }; |
882 | + browser.renderEditorial = function() { |
883 | + hits.renderEditorial = true; |
884 | + }; |
885 | + browser.renderSearchResults = function() { |
886 | + hits.renderSearchResults = true; |
887 | + }; |
888 | + // showView needs to be hacked because it does the rendering of |
889 | + // fullscreen/sidebar. |
890 | + browser.showView = function(view) { |
891 | + hits[view] = true; |
892 | + }; |
893 | + }); |
894 | + |
895 | + afterEach(function() { |
896 | + browser.destroy(); |
897 | + Y.one('#subapp-browser').remove(true); |
898 | + }); |
899 | + |
900 | + it('bws-sidebar dispatches correctly', function() { |
901 | + var req = { |
902 | + path: '/bws/sidebar/', |
903 | + params: { |
904 | + viewmode: 'sidebar' |
905 | + } |
906 | + }; |
907 | + var expected = Y.merge(hits, { |
908 | + sidebar: true, |
909 | + renderEditorial: true |
910 | + }); |
911 | + |
912 | + browser.routeView(req, undefined, function() {}); |
913 | + assert.deepEqual(hits, expected); |
914 | + }); |
915 | + |
916 | + it('bws-sidebar-charmid dispatches correctly', function() { |
917 | + var req = { |
918 | + path: '/bws/sidebar/precise/apache2-2', |
919 | + params: { |
920 | + viewmode: 'sidebar', |
921 | + id: 'precise/apache2-2' |
922 | + } |
923 | + }; |
924 | + var expected = Y.merge(hits, { |
925 | + sidebar: true, |
926 | + renderEditorial: true, |
927 | + renderCharmDetails: true |
928 | + }); |
929 | + |
930 | + browser.routeView(req, undefined, function() {}); |
931 | + assert.deepEqual(hits, expected); |
932 | + }); |
933 | + |
934 | + it('bws-sidebar-search-charmid dispatches correctly', function() { |
935 | + var req = { |
936 | + path: '/bws/sidebar/search/precise/apache2-2', |
937 | + params: { |
938 | + viewmode: 'sidebar', |
939 | + id: 'precise/apache2-2' |
940 | + } |
941 | + }; |
942 | + var expected = Y.merge(hits, { |
943 | + sidebar: true, |
944 | + renderSearchResults: true, |
945 | + renderCharmDetails: true |
946 | + }); |
947 | + |
948 | + browser.routeView(req, undefined, function() {}); |
949 | + assert.deepEqual(hits, expected); |
950 | + }); |
951 | + |
952 | + it('bws-fullscreen dispatches correctly', function() { |
953 | + var req = { |
954 | + path: '/bws/fullscreen/', |
955 | + params: { |
956 | + viewmode: 'fullscreen' |
957 | + } |
958 | + }; |
959 | + var expected = Y.merge(hits, { |
960 | + fullscreen: true, |
961 | + renderEditorial: true |
962 | + }); |
963 | + |
964 | + browser.routeView(req, undefined, function() {}); |
965 | + assert.deepEqual(hits, expected); |
966 | + }); |
967 | + |
968 | + it('fullscreen-charmid dispatches correctly', function() { |
969 | + var req = { |
970 | + path: '/bws/fullscreen/precise/apache2-2', |
971 | + params: { |
972 | + viewmode: 'fullscreen', |
973 | + id: 'precise/apache2-2' |
974 | + } |
975 | + }; |
976 | + var expected = Y.merge(hits, { |
977 | + fullscreen: true, |
978 | + renderCharmDetails: true |
979 | + }); |
980 | + |
981 | + browser.routeView(req, undefined, function() {}); |
982 | + assert.deepEqual(hits, expected); |
983 | + }); |
984 | + |
985 | + it('fullscreen-search-charmid dispatches correctly', function() { |
986 | + var req = { |
987 | + path: '/bws/fullscreen/search/precise/apache2-2', |
988 | + params: { |
989 | + viewmode: 'fullscreen', |
990 | + id: 'precise/apache2-2' |
991 | + } |
992 | + }; |
993 | + var expected = Y.merge(hits, { |
994 | + fullscreen: true, |
995 | + renderCharmDetails: true |
996 | + }); |
997 | + |
998 | + browser.routeView(req, undefined, function() {}); |
999 | + assert.deepEqual(hits, expected); |
1000 | + }); |
1001 | + |
1002 | + it('sidebar to sidebar-charmid dispatches correctly', function() { |
1003 | + var req = { |
1004 | + path: '/bws/sidebar/', |
1005 | + params: { |
1006 | + viewmode: 'sidebar' |
1007 | + } |
1008 | + }; |
1009 | + browser.routeView(req, undefined, function() {}); |
1010 | + |
1011 | + // Now route through to the charmid from here and we should not hit the |
1012 | + // editorial content again. |
1013 | + resetHits(); |
1014 | + req = { |
1015 | + path: '/bws/sidebar/precise/apache2-2', |
1016 | + params: { |
1017 | + viewmode: 'sidebar', |
1018 | + id: 'precise/apache2-2' |
1019 | + } |
1020 | + }; |
1021 | + |
1022 | + var expected = Y.merge(hits, { |
1023 | + renderCharmDetails: true |
1024 | + }); |
1025 | + |
1026 | + browser.routeView(req, undefined, function() {}); |
1027 | + assert.deepEqual(hits, expected); |
1028 | + }); |
1029 | + |
1030 | + it('sidebar-details to sidebar dispatchse correctly', function() { |
1031 | + var req = { |
1032 | + path: '/bws/sidebar/precise/apache2-2', |
1033 | + params: { |
1034 | + viewmode: 'sidebar', |
1035 | + id: 'precise/apache2-2' |
1036 | + } |
1037 | + }; |
1038 | + browser.routeView(req, undefined, function() {}); |
1039 | + |
1040 | + // Reset the hits and we should not redraw anything to update the view. |
1041 | + resetHits(); |
1042 | + req = { |
1043 | + path: '/bws/sidebar/', |
1044 | + params: { |
1045 | + viewmode: 'sidebar' |
1046 | + } |
1047 | + }; |
1048 | + |
1049 | + var expected = Y.merge(hits, {}); |
1050 | + browser.routeView(req, undefined, function() {}); |
1051 | + assert.deepEqual(hits, expected); |
1052 | + }); |
1053 | + |
1054 | + it('fullscreen to fullscreen-details dispatches correctly', function() { |
1055 | + var req = { |
1056 | + path: '/bws/fullscreen/', |
1057 | + params: { |
1058 | + viewmode: 'fullscreen' |
1059 | + } |
1060 | + }; |
1061 | + browser.routeView(req, undefined, function() {}); |
1062 | + |
1063 | + // Now route through to the charmid from here and we should not hit the |
1064 | + // editorial content again. |
1065 | + resetHits(); |
1066 | + req = { |
1067 | + path: '/bws/fullscreen/precise/apache2-2', |
1068 | + params: { |
1069 | + viewmode: 'fullscreen', |
1070 | + id: 'precise/apache2-2' |
1071 | + } |
1072 | + }; |
1073 | + |
1074 | + var expected = Y.merge(hits, { |
1075 | + renderCharmDetails: true |
1076 | + }); |
1077 | + |
1078 | + browser.routeView(req, undefined, function() {}); |
1079 | + assert.deepEqual(hits, expected); |
1080 | + }); |
1081 | + |
1082 | + it('fullscreen-details to fullscreen renders editorial', function() { |
1083 | + var req = { |
1084 | + path: '/bws/fullscreen/precise/apache2-2', |
1085 | + params: { |
1086 | + viewmode: 'fullscreen', |
1087 | + id: 'precise/apache2-2' |
1088 | + } |
1089 | + }; |
1090 | + browser.routeView(req, undefined, function() {}); |
1091 | + |
1092 | + // Reset the hits and we should not redraw anything to update the view. |
1093 | + resetHits(); |
1094 | + req = { |
1095 | + path: '/bws/fullscreen/', |
1096 | + params: { |
1097 | + viewmode: 'fullscreen' |
1098 | + } |
1099 | + }; |
1100 | + |
1101 | + var expected = Y.merge(hits, { |
1102 | + renderEditorial: true |
1103 | + }); |
1104 | + browser.routeView(req, undefined, function() {}); |
1105 | + assert.deepEqual(hits, expected); |
1106 | + }); |
1107 | + |
1108 | + }); |
1109 | })(); |
1110 | + |
Reviewers: mp+159215_ code.launchpad. net,
Message:
Please take a look.
Description:
TBD
https:/ /code.launchpad .net/~rharding/ juju-gui/ browser_ links/+ merge/159215
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/8726048/
Affected files: browser/ browser. js browser/ templates/ browser_ charm.handlebar s browser/ views/charm. js browser/ views/editorial .js browser/ views/view. js browser- search. handlebars charm-token. handlebars
A [revision details]
M app/subapps/
M app/subapps/
M app/subapps/
M app/subapps/
M app/subapps/
M app/templates/
M app/templates/