Merge lp:~rharding/juju-gui/browser_links into lp:juju-gui/experimental

Proposed by Richard Harding
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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+159215@code.launchpad.net

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.

https://codereview.appspot.com/8726048/

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

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:
   A [revision details]
   M app/subapps/browser/browser.js
   M app/subapps/browser/templates/browser_charm.handlebars
   M app/subapps/browser/views/charm.js
   M app/subapps/browser/views/editorial.js
   M app/subapps/browser/views/view.js
   M app/templates/browser-search.handlebars
   M app/templates/charm-token.handlebars

Revision history for this message
Richard Harding (rharding) wrote :
lp:~rharding/juju-gui/browser_links updated
579. By Richard Harding

Garden

Revision history for this message
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://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/browser.js#newcode30
app/subapps/browser/browser.js:30: */
Nitpicky, but your comment blocks seem to be misformatted. Shouldn't
there be stars as the leading character the whole way?

/**
   * Some text
   */

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/browser.js#newcode94
app/subapps/browser/browser.js:94: this._viewState =
Y.merge(this._oldState, {});
Is this necessary? Can't you just do this._viewState = {...} and skip
merging with {}?

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/browser.js#newcode219
app/subapps/browser/browser.js:219: var charmID = req.params.id;
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://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/browser.js#newcode260
app/subapps/browser/browser.js:260: renderEditorial: function(req, res,
next) {
Same here.

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/browser.js#newcode297
app/subapps/browser/browser.js:297: * Render the fullscreen view to the
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://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/templates/browser_charm.handlebars
File app/subapps/browser/templates/browser_charm.handlebars (right):

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/templates/browser_charm.handlebars#newcode4
app/subapps/browser/templates/browser_charm.handlebars:4: <a href=""
class="back">{{#if isFullscreen}}Back{{else}}Close{{/if}}</a>
We're going to be using some helper thing that generates the url here,
right?

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/views/editorial.js
File app/subapps/browser/views/editorial.js (right):

https://codereview.appspot.com/8726048/diff/3001/app/subapps/browser/views/editorial.js#newcode68
app/subapps/browser/views/editorial.js:68: *
If container isn't a parameter anymore, we're not loading into a
specified container.

https://codereview.appspot.com/8726048/

lp:~rharding/juju-gui/browser_links updated
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

Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
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://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode113
app/subapps/browser/browser.js:113: viewmode: undefined,
Why undefined? I expect undefined to be a state that has never been
examined/set. Why isn't null correct?

https://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode225
app/subapps/browser/browser.js:225: this._viewState.charmID = undefined;
Again, why define the value to be undefined? I think we mean the value
is null.

https://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode391
app/subapps/browser/browser.js:391:
Y.one('#subapp-browser').setStyle('display', 'block');
I think setStyle('display', 'assumed-valid-state') is bad. I see it is
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://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode437
app/subapps/browser/browser.js:437: // Else render the editorial
content.
This is not an else. I can render search and editorial according to this
logic.

https://codereview.appspot.com/8726048/

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

Feedback below.

https://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode113
app/subapps/browser/browser.js:113: viewmode: undefined,
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://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode225
app/subapps/browser/browser.js:225: this._viewState.charmID = undefined;
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://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode391
app/subapps/browser/browser.js:391:
Y.one('#subapp-browser').setStyle('display', 'block');
On 2013/04/19 15:36:54, curtis wrote:
> I think setStyle('display', 'assumed-valid-state') is bad. I see it is
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://codereview.appspot.com/8726048/diff/9001/app/subapps/browser/browser.js#newcode437
app/subapps/browser/browser.js:437: // Else render the editorial
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.

https://codereview.appspot.com/8726048/

lp:~rharding/juju-gui/browser_links updated
585. By Richard Harding

Fix pre review

Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
Curtis Hovey (sinzui) wrote :
Revision history for this message
Jeff Pihach (hatch) wrote :

LGTM - There are a few rename and documentation requests below but good
work, thanks for working through this!

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/browser.js#newcode169
app/subapps/browser/browser.js:169: _stateChanged: function(field) {
These 4 methods which return true/false don't actually do what their
method names imply.

if (this._hasStateChanged()) {} would be easier to follow as you can
assume that there are no side effects.

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/browser.js#newcode305
app/subapps/browser/browser.js:305: Y.one('.bws-view-data').show();
Where does this element come from? Isn't it being rendered to the DOM
with the render() above?

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/browser.js#newcode355
app/subapps/browser/browser.js:355: renderSearchResults: function(req,
res, next) {
linter might complain about this not being documented with @method

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/browser.js#newcode379
app/subapps/browser/browser.js:379: if (this._showCharm()) {
See above comments - this looks like it's showing the charm and then
checking for a truthy return value that it's succeeded

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/views/editorial.js
File app/subapps/browser/views/editorial.js (right):

https://codereview.appspot.com/8726048/diff/15001/app/subapps/browser/views/editorial.js#newcode171
app/subapps/browser/views/editorial.js:171: renderTo: {
could we document these attrs please.

https://codereview.appspot.com/8726048/diff/15001/app/templates/charm-token.handlebars
File app/templates/charm-token.handlebars (right):

https://codereview.appspot.com/8726048/diff/15001/app/templates/charm-token.handlebars#newcode8
app/templates/charm-token.handlebars:8: <div class="charm-icon"></div>
I think the indentation is off here.

https://codereview.appspot.com/8726048/diff/15001/test/test_browser_app.js
File test/test_browser_app.js (right):

https://codereview.appspot.com/8726048/diff/15001/test/test_browser_app.js#newcode333
test/test_browser_app.js:333: it('sidebar to details does no rerender
sidebar', function() {
what is this doing?

'renders details without re-rendering sidebar' ?

There are a few which aren't clear from their titles

https://codereview.appspot.com/8726048/

lp:~rharding/juju-gui/browser_links updated
586. By Richard Harding

Updates per code review

Revision history for this message
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://codereview.appspot.com/8726048

https://codereview.appspot.com/8726048/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+

Subscribers

People subscribed via source and target branches